Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2024-01-10 23:27:55 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-10 23:27:55 +0300
commit8e6173652867fad53710575c794e50dba6819cb7 (patch)
treeb652a63603188f2d380c567080d57845539bd042
parenta2421d9b73f934f82f1eea995b2933d6eec37135 (diff)
Add latest changes from gitlab-org/security/gitlab@16-6-stable-ee
-rw-r--r--app/controllers/projects/integrations/slash_commands_controller.rb81
-rw-r--r--app/models/chat_name.rb7
-rw-r--r--app/models/concerns/recoverable_by_any_email.rb27
-rw-r--r--app/models/integrations/base_slash_commands.rb41
-rw-r--r--app/models/integrations/mattermost_slash_commands.rb17
-rw-r--r--app/models/integrations/slack_slash_commands.rb14
-rw-r--r--app/models/merge_request.rb4
-rw-r--r--app/views/devise/passwords/new.html.haml2
-rw-r--r--app/views/projects/integrations/slash_commands/show.html.haml30
-rw-r--r--config/routes/project.rb3
-rw-r--r--db/migrate/20231215135014_add_token_to_chat_names.rb12
-rw-r--r--db/schema_migrations/202312151350141
-rw-r--r--db/structure.sql4
-rw-r--r--lib/gitlab/slash_commands/presenters/access.rb11
-rw-r--r--lib/gitlab/slash_commands/verify_request.rb50
-rw-r--r--locale/gitlab.pot33
-rw-r--r--spec/controllers/passwords_controller_spec.rb131
-rw-r--r--spec/factories/chat_names.rb2
-rw-r--r--spec/lib/gitlab/slash_commands/presenters/access_spec.rb15
-rw-r--r--spec/lib/gitlab/slash_commands/verify_request_spec.rb36
-rw-r--r--spec/mailers/devise_mailer_spec.rb8
-rw-r--r--spec/models/concerns/recoverable_by_any_email_spec.rb133
-rw-r--r--spec/models/integrations/mattermost_slash_commands_spec.rb34
-rw-r--r--spec/models/integrations/slack_slash_commands_spec.rb23
-rw-r--r--spec/models/merge_request_spec.rb30
-rw-r--r--spec/requests/projects/integrations/slash_commands_controller_spec.rb139
-rw-r--r--spec/support/helpers/email_helpers.rb12
-rw-r--r--spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb72
28 files changed, 883 insertions, 89 deletions
diff --git a/app/controllers/projects/integrations/slash_commands_controller.rb b/app/controllers/projects/integrations/slash_commands_controller.rb
new file mode 100644
index 00000000000..891a7c1a749
--- /dev/null
+++ b/app/controllers/projects/integrations/slash_commands_controller.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module Projects
+ module Integrations
+ class SlashCommandsController < Projects::ApplicationController
+ before_action :authenticate_user!
+
+ feature_category :integrations
+
+ def show
+ @redirect_url = integration_redirect_url
+
+ unless valid_request?
+ @error = s_("Integrations|The slash command verification request has expired. Please run the command again.")
+ return
+ end
+
+ return if valid_user? || @redirect_url.blank?
+
+ @error = s_("Integrations|The slash command request is invalid.")
+ end
+
+ def confirm
+ if valid_request? && valid_user?
+ Gitlab::SlashCommands::VerifyRequest.new(integration, chat_user, request_params[:response_url]).approve!
+ redirect_to request_params[:redirect_url]
+ else
+ @error = s_("Integrations|The slash command request is invalid.")
+ render :show
+ end
+ end
+
+ private
+
+ def request_params
+ params.permit(:integration, :team, :channel, :response_url, :command_id, :redirect_url)
+ end
+
+ def cached_params
+ @cached_params ||= Rails.cache.fetch(cache_key)
+ end
+
+ def cache_key
+ @cache_key ||= Kernel.format(::Integrations::BaseSlashCommands::CACHE_KEY, secret: request_params[:command_id])
+ end
+
+ def integration
+ integration = request_params[:integration]
+
+ case integration
+ when 'slack_slash_commands'
+ project.slack_slash_commands_integration
+ when 'mattermost_slash_commands'
+ project.mattermost_slash_commands_integration
+ end
+ end
+
+ def integration_redirect_url
+ return unless integration
+
+ team, channel, url = request_params.values_at(:team, :channel, :response_url)
+
+ integration.redirect_url(team, channel, url)
+ end
+
+ def valid_request?
+ cached_params.present?
+ end
+
+ def valid_user?
+ return false unless chat_user
+
+ current_user == chat_user.user
+ end
+
+ def chat_user
+ @chat_user ||= ChatNames::FindUserService.new(cached_params[:team_id], cached_params[:user_id]).execute
+ end
+ end
+ end
+end
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
index 38e6273bf20..b413da01f24 100644
--- a/app/models/chat_name.rb
+++ b/app/models/chat_name.rb
@@ -11,6 +11,13 @@ class ChatName < ApplicationRecord
validates :chat_id, uniqueness: { scope: :team_id }
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm',
+ key: Settings.attr_encrypted_db_key_base_32,
+ encode: false,
+ encode_iv: false
+
# Updates the "last_used_timestamp" but only if it wasn't already updated
# recently.
#
diff --git a/app/models/concerns/recoverable_by_any_email.rb b/app/models/concerns/recoverable_by_any_email.rb
index 3a56e58ca00..7bd908597c9 100644
--- a/app/models/concerns/recoverable_by_any_email.rb
+++ b/app/models/concerns/recoverable_by_any_email.rb
@@ -1,39 +1,34 @@
# frozen_string_literal: true
-# Concern that overrides the Devise methods
-# to send reset password instructions to any verified user email
+# Concern that overrides the Devise methods to allow reset password instructions
+# to be sent to any users' confirmed secondary emails.
+# See https://github.com/heartcombo/devise/blob/main/lib/devise/models/recoverable.rb
module RecoverableByAnyEmail
extend ActiveSupport::Concern
class_methods do
def send_reset_password_instructions(attributes = {})
- email = attributes.delete(:email)
- super unless email
+ return super unless attributes[:email]
- recoverable = by_email_with_errors(email)
- recoverable.send_reset_password_instructions if recoverable&.persisted?
- recoverable
- end
+ email = Email.confirmed.find_by(email: attributes[:email].to_s)
+ return super unless email
- private
+ recoverable = email.user
- def by_email_with_errors(email)
- record = find_by_any_email(email, confirmed: true) || new
- record.errors.add(:email, :invalid) unless record.persisted?
- record
+ recoverable.send_reset_password_instructions(to: email.email)
+ recoverable
end
end
- def send_reset_password_instructions
+ def send_reset_password_instructions(opts = {})
token = set_reset_password_token
- opts = { to: verified_emails(include_private_email: false) }
send_reset_password_instructions_notification(token, opts)
token
end
- private
+ protected
def send_reset_password_instructions_notification(token, opts = {})
send_devise_notification(:reset_password_instructions, token, opts)
diff --git a/app/models/integrations/base_slash_commands.rb b/app/models/integrations/base_slash_commands.rb
index 58821e5fb4e..47b2e89a25e 100644
--- a/app/models/integrations/base_slash_commands.rb
+++ b/app/models/integrations/base_slash_commands.rb
@@ -4,6 +4,9 @@
# This class is not meant to be used directly, but only to inherrit from.
module Integrations
class BaseSlashCommands < Integration
+ CACHE_KEY = "slash-command-requests:%{secret}"
+ CACHE_EXPIRATION_TIME = 3.minutes
+
attribute :category, default: 'chat'
def valid_token?(token)
@@ -26,32 +29,44 @@ module Integrations
chat_user = find_chat_user(params)
user = chat_user&.user
- if user
- unless user.can?(:use_slash_commands)
- return Gitlab::SlashCommands::Presenters::Access.new.deactivated if user.deactivated?
+ return unknown_user_message(params) unless user
+
+ unless user.can?(:use_slash_commands)
+ return Gitlab::SlashCommands::Presenters::Access.new.deactivated if user.deactivated?
- return Gitlab::SlashCommands::Presenters::Access.new.access_denied(project)
- end
+ return Gitlab::SlashCommands::Presenters::Access.new.access_denied(project)
+ end
+ if Gitlab::SlashCommands::VerifyRequest.new(self, chat_user).valid?
Gitlab::SlashCommands::Command.new(project, chat_user, params).execute
else
- url = authorize_chat_name_url(params)
- Gitlab::SlashCommands::Presenters::Access.new(url).authorize
+ command_id = cache_slash_commands_request!(params)
+ Gitlab::SlashCommands::Presenters::Access.new.confirm(confirmation_url(command_id, params))
end
end
private
- # rubocop: disable CodeReuse/ServiceClass
def find_chat_user(params)
- ChatNames::FindUserService.new(params[:team_id], params[:user_id]).execute
+ ChatNames::FindUserService.new(params[:team_id], params[:user_id]).execute # rubocop: disable CodeReuse/ServiceClass -- This is not AR
end
- # rubocop: enable CodeReuse/ServiceClass
- # rubocop: disable CodeReuse/ServiceClass
def authorize_chat_name_url(params)
- ChatNames::AuthorizeUserService.new(params).execute
+ ChatNames::AuthorizeUserService.new(params).execute # rubocop: disable CodeReuse/ServiceClass -- This is not AR
+ end
+
+ def unknown_user_message(params)
+ url = authorize_chat_name_url(params)
+ Gitlab::SlashCommands::Presenters::Access.new(url).authorize
+ end
+
+ def cache_slash_commands_request!(params)
+ secret = SecureRandom.uuid
+ Kernel.format(CACHE_KEY, secret: secret).tap do |cache_key|
+ Rails.cache.write(cache_key, params, expires_in: CACHE_EXPIRATION_TIME)
+ end
+
+ secret
end
- # rubocop: enable CodeReuse/ServiceClass
end
end
diff --git a/app/models/integrations/mattermost_slash_commands.rb b/app/models/integrations/mattermost_slash_commands.rb
index 9554dec4168..29ed563a902 100644
--- a/app/models/integrations/mattermost_slash_commands.rb
+++ b/app/models/integrations/mattermost_slash_commands.rb
@@ -4,6 +4,8 @@ module Integrations
class MattermostSlashCommands < BaseSlashCommands
include Ci::TriggersHelper
+ MATTERMOST_URL = '%{ORIGIN}/%{TEAM}/channels/%{CHANNEL}'
+
field :token,
type: :password,
non_empty_password_title: -> { s_('ProjectService|Enter new token') },
@@ -41,6 +43,21 @@ module Integrations
[[], e.message]
end
+ def redirect_url(team, channel, url)
+ return if Gitlab::UrlBlocker.blocked_url?(url, schemes: %w[http https], enforce_sanitization: true)
+
+ origin = Addressable::URI.parse(url).origin
+ format(MATTERMOST_URL, ORIGIN: origin, TEAM: team, CHANNEL: channel)
+ end
+
+ def confirmation_url(command_id, params)
+ team, channel, response_url = params.values_at(:team_domain, :channel_name, :response_url)
+
+ Rails.application.routes.url_helpers.project_integrations_slash_commands_url(
+ project, command_id: command_id, integration: to_param, team: team, channel: channel, response_url: response_url
+ )
+ end
+
private
def command(params)
diff --git a/app/models/integrations/slack_slash_commands.rb b/app/models/integrations/slack_slash_commands.rb
index c5ea6f22951..401ef4fb9fc 100644
--- a/app/models/integrations/slack_slash_commands.rb
+++ b/app/models/integrations/slack_slash_commands.rb
@@ -4,6 +4,8 @@ module Integrations
class SlackSlashCommands < BaseSlashCommands
include Ci::TriggersHelper
+ SLACK_REDIRECT_URL = 'slack://channel?team=%{TEAM}&id=%{CHANNEL}'
+
field :token,
type: :password,
non_empty_password_title: -> { s_('ProjectService|Enter new token') },
@@ -29,6 +31,18 @@ module Integrations
end
end
+ def redirect_url(team, channel, _url)
+ Kernel.format(SLACK_REDIRECT_URL, TEAM: team, CHANNEL: channel)
+ end
+
+ def confirmation_url(command_id, params)
+ team, channel, response_url = params.values_at(:team_id, :channel_id, :response_url)
+
+ Rails.application.routes.url_helpers.project_integrations_slash_commands_url(
+ project, command_id: command_id, integration: to_param, team: team, channel: channel, response_url: response_url
+ )
+ end
+
private
def format(text)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 524a9b8074b..807e0e3b6cf 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1158,6 +1158,10 @@ class MergeRequest < ApplicationRecord
end
end
+ def previous_diff
+ merge_request_diffs.order(id: :desc).offset(1).take
+ end
+
def version_params_for(diff_refs)
if diff = merge_request_diff_for(diff_refs)
{ diff_id: diff.id }
diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml
index 8e55977fe7a..227418e366d 100644
--- a/app/views/devise/passwords/new.html.haml
+++ b/app/views/devise/passwords/new.html.haml
@@ -7,7 +7,7 @@
= f.label :email, _('Email')
= f.email_field :email, class: "form-control gl-form-input", required: true, autocomplete: 'off', value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.')
.form-text.text-muted
- = _('Requires a verified GitLab email address.')
+ = _('Requires your primary or verified secondary GitLab email address.')
- if recaptcha_enabled?
.gl-mb-5
diff --git a/app/views/projects/integrations/slash_commands/show.html.haml b/app/views/projects/integrations/slash_commands/show.html.haml
new file mode 100644
index 00000000000..4f91f3fcbd2
--- /dev/null
+++ b/app/views/projects/integrations/slash_commands/show.html.haml
@@ -0,0 +1,30 @@
+- breadcrumb_title s_('Integrations|Base slash commands')
+- page_title s_('Integrations|Base slash commands')
+- integration_name = params[:integration].titleize
+%main{ role: 'main' }
+ .gl-max-w-80.gl-mx-auto.gl-mt-6
+ = render Pajamas::CardComponent.new do |c|
+ - c.with_header do
+ %h4.gl-m-0= sprintf(s_('Integrations|Authorize %{integration_name} (%{user}) to use your account?'), { user: current_user.username, integration_name: integration_name })
+ - c.with_body do
+ %p
+ = sprintf(s_('Integrations|An application called %{integration_name} is requesting access to your GitLab account.'), { integration_name: integration_name })
+ %p
+ = _('This application will be able to:')
+ %ul
+ %li= s_('SlackIntegration|Perform deployments.')
+ %li= s_('SlackIntegration|Run ChatOps jobs.')
+ %h4.gl-mb-0
+ = @error
+ - c.with_footer do
+ .gl-display-flex
+ - if @error.nil?
+ = form_tag confirm_project_integrations_slash_commands_path(@project), method: :post do
+ = hidden_field_tag :command_id, params[:command_id]
+ = hidden_field_tag :response_url, params[:response_url]
+ = hidden_field_tag :integration, params[:integration]
+ = hidden_field_tag :redirect_url, @redirect_url
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :danger) do
+ = _('Authorize')
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: @redirect_url, button_options: { class: 'gl-ml-3' }) do
+ = s_('Integrations|Go back to your workspace')
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 947ed6b5413..577f61b5b81 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -439,6 +439,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
namespace :integrations do
resource :shimo, only: [:show]
+ resource :slash_commands, only: [:show] do
+ post :confirm
+ end
end
get :planning_hierarchy
diff --git a/db/migrate/20231215135014_add_token_to_chat_names.rb b/db/migrate/20231215135014_add_token_to_chat_names.rb
new file mode 100644
index 00000000000..b5c867a31bd
--- /dev/null
+++ b/db/migrate/20231215135014_add_token_to_chat_names.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class AddTokenToChatNames < Gitlab::Database::Migration[2.2]
+ enable_lock_retries!
+
+ milestone '16.6'
+
+ def change
+ add_column :chat_names, :encrypted_token, :binary
+ add_column :chat_names, :encrypted_token_iv, :binary
+ end
+end
diff --git a/db/schema_migrations/20231215135014 b/db/schema_migrations/20231215135014
new file mode 100644
index 00000000000..9b9659b0a03
--- /dev/null
+++ b/db/schema_migrations/20231215135014
@@ -0,0 +1 @@
+79805fe9334b0ab5324698d6861e93f2e7c5c6f6cd35813bb753f3125b4ed212 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index dd784030356..bdbdbfb277d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -13502,7 +13502,9 @@ CREATE TABLE chat_names (
chat_name character varying,
last_used_at timestamp without time zone,
created_at timestamp without time zone NOT NULL,
- updated_at timestamp without time zone NOT NULL
+ updated_at timestamp without time zone NOT NULL,
+ encrypted_token bytea,
+ encrypted_token_iv bytea
);
CREATE SEQUENCE chat_names_id_seq
diff --git a/lib/gitlab/slash_commands/presenters/access.rb b/lib/gitlab/slash_commands/presenters/access.rb
index e098762f290..56a960c9bbf 100644
--- a/lib/gitlab/slash_commands/presenters/access.rb
+++ b/lib/gitlab/slash_commands/presenters/access.rb
@@ -42,6 +42,17 @@ module Gitlab
ephemeral_response(text: message)
end
+
+ def confirm(url)
+ text = [
+ _("To ensure the highest security standards, we verify the source of all slash commands."),
+ Kernel.format(_("Please confirm the request by accessing %{url} through a web browser."),
+ url: "<#{url}|this link>"),
+ _("Upon successful validation, you're granted access to slash commands.")
+ ].join("\n\n")
+
+ ephemeral_response(text: text)
+ end
end
end
end
diff --git a/lib/gitlab/slash_commands/verify_request.rb b/lib/gitlab/slash_commands/verify_request.rb
new file mode 100644
index 00000000000..41f71064573
--- /dev/null
+++ b/lib/gitlab/slash_commands/verify_request.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SlashCommands
+ class VerifyRequest
+ attr_accessor :integration, :chat_name, :response_url
+
+ def initialize(integration, chat_name, response_url = nil)
+ @integration = integration
+ @chat_name = chat_name
+ @response_url = response_url
+ end
+
+ def approve!
+ update_token!
+ update_source_message
+ end
+
+ def valid?
+ return false if integration.token.nil? || chat_name.token.nil?
+
+ ActiveSupport::SecurityUtils.secure_compare(integration.token, chat_name.token)
+ end
+
+ private
+
+ def update_token!
+ chat_name.update!(token: integration.token)
+ end
+
+ def update_source_message
+ request_body = Gitlab::Json.dump(verified_request_body)
+
+ Gitlab::HTTP.post(response_url, body: request_body, headers: headers)
+ end
+
+ def verified_request_body
+ {
+ 'replace_original' => 'true',
+ 'text' => _("You've successfully verified! You now have access to slash commands. " \
+ "Thanks for helping ensure security!")
+ }
+ end
+
+ def headers
+ { 'Content-Type' => 'application/json' }
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5f012568cde..6a769ad58c2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -25587,6 +25587,9 @@ msgstr ""
msgid "Integrations|All projects inheriting these settings will also be reset."
msgstr ""
+msgid "Integrations|An application called %{integration_name} is requesting access to your GitLab account."
+msgstr ""
+
msgid "Integrations|An application called %{integration_name} is requesting access to your GitLab account. This application was created by GitLab Inc."
msgstr ""
@@ -25599,6 +25602,9 @@ msgstr ""
msgid "Integrations|Authorize %{integration_name} (%{user}) to use your account?"
msgstr ""
+msgid "Integrations|Base slash commands"
+msgstr ""
+
msgid "Integrations|Branches for which notifications are to be sent"
msgstr ""
@@ -25671,6 +25677,9 @@ msgstr ""
msgid "Integrations|GitLab for Slack app must be reinstalled to enable notifications"
msgstr ""
+msgid "Integrations|Go back to your workspace"
+msgstr ""
+
msgid "Integrations|Group-level integration management"
msgstr ""
@@ -25743,6 +25752,12 @@ msgstr ""
msgid "Integrations|Standard"
msgstr ""
+msgid "Integrations|The slash command request is invalid."
+msgstr ""
+
+msgid "Integrations|The slash command verification request has expired. Please run the command again."
+msgstr ""
+
msgid "Integrations|There are no projects using custom settings"
msgstr ""
@@ -35674,6 +35689,9 @@ msgstr ""
msgid "Please complete your profile with email address"
msgstr ""
+msgid "Please confirm the request by accessing %{url} through a web browser."
+msgstr ""
+
msgid "Please confirm your email address"
msgstr ""
@@ -40581,15 +40599,15 @@ msgid_plural "Requires %{count} approvals from %{names}."
msgstr[0] ""
msgstr[1] ""
-msgid "Requires a verified GitLab email address."
-msgstr ""
-
msgid "Requires you to deploy or set up cloud-hosted Sentry."
msgstr ""
msgid "Requires your primary GitLab email address."
msgstr ""
+msgid "Requires your primary or verified secondary GitLab email address."
+msgstr ""
+
msgid "Resend"
msgstr ""
@@ -50123,6 +50141,9 @@ msgstr ""
msgid "To ensure no loss of personal content, this account should only be used for matters related to %{group_name}."
msgstr ""
+msgid "To ensure the highest security standards, we verify the source of all slash commands."
+msgstr ""
+
msgid "To find the state of this project's repository at the time of any of these versions, check out %{link_start}the tags%{link_end}"
msgstr ""
@@ -51650,6 +51671,9 @@ msgstr ""
msgid "Uploading: %{progress}"
msgstr ""
+msgid "Upon successful validation, you're granted access to slash commands."
+msgstr ""
+
msgid "Upstream"
msgstr ""
@@ -55795,6 +55819,9 @@ msgid_plural "You've successfully purchased the %{plan} plan subscription for %{
msgstr[0] ""
msgstr[1] ""
+msgid "You've successfully verified! You now have access to slash commands. Thanks for helping ensure security!"
+msgstr ""
+
msgid "YouTube"
msgstr ""
diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb
index aad946acad4..cff84da7382 100644
--- a/spec/controllers/passwords_controller_spec.rb
+++ b/spec/controllers/passwords_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PasswordsController do
+RSpec.describe PasswordsController, feature_category: :system_access do
include DeviseHelpers
before do
@@ -109,8 +109,9 @@ RSpec.describe PasswordsController do
describe '#create' do
let(:user) { create(:user) }
+ let(:email) { user.email }
- subject(:perform_request) { post(:create, params: { user: { email: user.email } }) }
+ subject(:perform_request) { post(:create, params: { user: { email: email } }) }
context 'when reCAPTCHA is disabled' do
before do
@@ -161,5 +162,131 @@ RSpec.describe PasswordsController do
expect(flash[:notice]).to include 'If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.'
end
end
+
+ context "sending 'Reset password instructions' email" do
+ include EmailHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_confirmed_primary_email) { user.email }
+ let_it_be(:user_confirmed_secondary_email) { create(:email, :confirmed, user: user, email: 'confirmed-secondary-email@example.com').email }
+ let_it_be(:user_unconfirmed_secondary_email) { create(:email, user: user, email: 'unconfirmed-secondary-email@example.com').email }
+ let_it_be(:unknown_email) { 'attacker@example.com' }
+ let_it_be(:invalid_email) { 'invalid_email' }
+ let_it_be(:sql_injection_email) { 'sql-injection-email@example.com OR 1=1' }
+ let_it_be(:another_user_confirmed_primary_email) { create(:user).email }
+ let_it_be(:another_user_unconfirmed_primary_email) { create(:user, :unconfirmed).email }
+
+ before do
+ reset_delivered_emails!
+
+ perform_request
+
+ perform_enqueued_jobs
+ end
+
+ context "when email param matches user's confirmed primary email" do
+ let(:email) { user_confirmed_primary_email }
+
+ it 'sends email to the primary email only' do
+ expect_only_one_email_to_be_sent(subject: 'Reset password instructions', to: [user_confirmed_primary_email])
+ end
+ end
+
+ context "when email param matches user's unconfirmed primary email" do
+ let(:email) { another_user_unconfirmed_primary_email }
+
+ # By default 'devise' gem allows password reset by unconfirmed primary email.
+ # When user account with unconfirmed primary email that means it is unconfirmed.
+ #
+ # Password reset by unconfirmed primary email is very helpful from
+ # security perspective. Example:
+ # Malicious person creates user account on GitLab with someone's email.
+ # If the email owner confirms the email for newly created account, the malicious person will be able
+ # to sign in into the account by password they provided during account signup.
+ # The malicious person could set up 2FA to the user account, after that
+ # te email owner would not able to get access to that user account even
+ # after performing password reset.
+ # To deal with that case safely the email owner should reset password
+ # for the user account first. That will make sure that after the user account
+ # is confirmed the malicious person is not be able to sign in with
+ # the password they provided during the account signup. Then email owner
+ # could sign into the account, they will see a prompt to confirm the account email
+ # to proceed. They can safely confirm the email and take over the account.
+ # That is one of the reasons why password reset by unconfirmed primary email should be allowed.
+ it 'sends email to the primary email only' do
+ expect_only_one_email_to_be_sent(subject: 'Reset password instructions', to: [another_user_unconfirmed_primary_email])
+ end
+ end
+
+ context "when email param matches user's confirmed secondary email" do
+ let(:email) { user_confirmed_secondary_email }
+
+ it 'sends email to the confirmed secondary email only' do
+ expect_only_one_email_to_be_sent(subject: 'Reset password instructions', to: [user_confirmed_secondary_email])
+ end
+ end
+
+ # While unconfirmed primary emails are linked with users accounts,
+ # unconfirmed secondary emails should not be linked with any users till they are confirmed
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/356665
+ #
+ # In https://gitlab.com/gitlab-org/gitlab/-/issues/367823, it is considerd
+ # to prevent reserving emails on Gitlab by unconfirmed secondary emails.
+ # As per this issue, there might be cases that there are multiple users
+ # with the same unconfirmed secondary emails. It would be impossible to identify for
+ # what user account password reset is requested if password reset were allowed
+ # by unconfirmed secondary emails.
+ # Also note that it is not possible to request email confirmation for
+ # unconfirmed secondary emails without having access to the user account.
+ context "when email param matches user's unconfirmed secondary email" do
+ let(:email) { user_unconfirmed_secondary_email }
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when email param is unknown email' do
+ let(:email) { unknown_email }
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when email param is invalid email' do
+ let(:email) { invalid_email }
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when email param with attempt to cause SQL injection' do
+ let(:email) { sql_injection_email }
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/436084
+ context 'when email param with multiple emails' do
+ let(:email) do
+ [
+ user_confirmed_primary_email,
+ user_confirmed_secondary_email,
+ user_unconfirmed_secondary_email,
+ unknown_email,
+ another_user_confirmed_primary_email,
+ another_user_unconfirmed_primary_email
+ ]
+ end
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+ end
end
end
diff --git a/spec/factories/chat_names.rb b/spec/factories/chat_names.rb
index c872694ee64..ee29f0fa0e4 100644
--- a/spec/factories/chat_names.rb
+++ b/spec/factories/chat_names.rb
@@ -7,6 +7,8 @@ FactoryBot.define do
team_id { 'T0001' }
team_domain { 'Awesome Team' }
+ token { SecureRandom.uuid }
+
sequence(:chat_id) { |n| "U#{n}" }
chat_name { generate(:username) }
end
diff --git a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
index 3af0ae03256..d83871d67a2 100644
--- a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
+++ b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
@@ -76,4 +76,19 @@ RSpec.describe Gitlab::SlashCommands::Presenters::Access do
end
end
end
+
+ describe '#confirm' do
+ let(:url) { 'https://example.com/api' }
+
+ subject { described_class.new.confirm(url) }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'tells the user to confirm the request' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match(
+ "Please confirm the request by accessing <#{url}|this link> through a web browser"
+ )
+ end
+ end
end
diff --git a/spec/lib/gitlab/slash_commands/verify_request_spec.rb b/spec/lib/gitlab/slash_commands/verify_request_spec.rb
new file mode 100644
index 00000000000..df6052ded93
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/verify_request_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SlashCommands::VerifyRequest, feature_category: :integrations do
+ let_it_be(:integration) { create(:slack_slash_commands_integration) }
+ let_it_be(:chat_name) { create(:chat_name) }
+ let(:response_url) { 'http://www.example.com/' }
+
+ subject(:verification) { described_class.new(integration, chat_name, response_url) }
+
+ describe '#approve!' do
+ before do
+ stub_request(:post, "http://www.example.com/").to_return(status: 200, body: 'ok')
+ end
+
+ it 'updates the token' do
+ expect { verification.approve! }.to change { chat_name.reload.token }.to(integration.token)
+ end
+
+ it 'updates the ephemeral message' do
+ expect(Gitlab::HTTP).to receive(:post).with(
+ response_url, a_hash_including(body: an_instance_of(String), headers: an_instance_of(Hash))
+ ).once
+
+ verification.approve!
+ end
+ end
+
+ describe '#valid?' do
+ it 'compares tokens' do
+ expect(ActiveSupport::SecurityUtils).to receive(:secure_compare).with(integration.token, chat_name.token)
+ verification.valid?
+ end
+ end
+end
diff --git a/spec/mailers/devise_mailer_spec.rb b/spec/mailers/devise_mailer_spec.rb
index 0a6d38996b7..af3a9457527 100644
--- a/spec/mailers/devise_mailer_spec.rb
+++ b/spec/mailers/devise_mailer_spec.rb
@@ -103,10 +103,10 @@ RSpec.describe DeviseMailer, feature_category: :user_management do
describe '#reset_password_instructions' do
let_it_be(:user) { create(:user) }
- let(:params) { {} }
+ let(:opts) { {} }
subject do
- described_class.reset_password_instructions(user, 'faketoken', params)
+ described_class.reset_password_instructions(user, 'faketoken', opts)
end
it_behaves_like 'an email sent from GitLab'
@@ -139,9 +139,9 @@ RSpec.describe DeviseMailer, feature_category: :user_management do
is_expected.to have_header 'X-Mailgun-Suppressions-Bypass', 'true'
end
- context 'with email in params' do
+ context 'with email in opts' do
let(:email) { 'example@example.com' }
- let(:params) { { to: email } }
+ let(:opts) { { to: email } }
it 'is sent to the specified email' do
is_expected.to deliver_to email
diff --git a/spec/models/concerns/recoverable_by_any_email_spec.rb b/spec/models/concerns/recoverable_by_any_email_spec.rb
index c17507cae83..ba0bb99effb 100644
--- a/spec/models/concerns/recoverable_by_any_email_spec.rb
+++ b/spec/models/concerns/recoverable_by_any_email_spec.rb
@@ -4,67 +4,140 @@ require 'spec_helper'
RSpec.describe RecoverableByAnyEmail, feature_category: :system_access do
describe '.send_reset_password_instructions' do
- let_it_be(:user) { create(:user, email: 'test@example.com') }
- let_it_be(:verified_email) { create(:email, :confirmed, user: user) }
- let_it_be(:unverified_email) { create(:email, user: user) }
+ include EmailHelpers
subject(:send_reset_password_instructions) do
User.send_reset_password_instructions(email: email)
end
- shared_examples 'sends the password reset email' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_confirmed_primary_email) { user.email }
+
+ let_it_be(:user_confirmed_secondary_email) do
+ create(:email, :confirmed, user: user, email: 'confirmed-secondary-email@example.com').email
+ end
+
+ let_it_be(:user_unconfirmed_secondary_email) do
+ create(:email, user: user, email: 'unconfirmed-secondary-email@example.com').email
+ end
+
+ let_it_be(:unknown_email) { 'attacker@example.com' }
+ let_it_be(:invalid_email) { 'invalid_email' }
+ let_it_be(:sql_injection_email) { 'sql-injection-email@example.com OR 1=1' }
+
+ let_it_be(:another_user_confirmed_primary_email) { create(:user).email }
+
+ let_it_be(:another_user) { create(:user, :unconfirmed) }
+ let_it_be(:another_user_unconfirmed_primary_email) { another_user.email }
+
+ shared_examples "sends 'Reset password instructions' email" do
it 'finds the user' do
- expect(send_reset_password_instructions).to eq(user)
+ expect(send_reset_password_instructions).to eq(expected_user)
end
it 'sends the email' do
+ reset_delivered_emails!
+
expect { send_reset_password_instructions }.to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+
+ perform_enqueued_jobs
+
+ expect_only_one_email_to_be_sent(subject: 'Reset password instructions', to: [email])
end
end
- shared_examples 'does not send the password reset email' do
+ shared_examples "does not send 'Reset password instructions' email" do
+ # If user is not found, returns a new user with errors.
+ # See https://github.com/heartcombo/devise/blob/main/lib/devise/models/recoverable.rb
it 'does not find the user' do
- expect(subject.id).to be_nil
- expect(subject.errors).not_to be_empty
+ expect(send_reset_password_instructions).to be_instance_of User
+ expect(send_reset_password_instructions).to be_new_record
+ expect(send_reset_password_instructions.errors).not_to be_empty
end
- it 'does not send any email' do
- subject
+ it 'does not send email to anyone' do
+ reset_delivered_emails!
+
+ expect { send_reset_password_instructions }
+ .not_to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+
+ perform_enqueued_jobs
- expect { subject }.not_to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+ should_not_email_anyone
end
end
- context 'with user primary email' do
- let(:email) { user.email }
+ context "when email param matches user's confirmed primary email" do
+ let(:expected_user) { user }
+ let(:email) { user_confirmed_primary_email }
- it_behaves_like 'sends the password reset email'
+ it_behaves_like "sends 'Reset password instructions' email"
end
- context 'with user verified email' do
- let(:email) { verified_email.email }
+ context "when email param matches user's unconfirmed primary email" do
+ let(:expected_user) { another_user }
+ let(:email) { another_user_unconfirmed_primary_email }
- it_behaves_like 'sends the password reset email'
+ it_behaves_like "sends 'Reset password instructions' email"
end
- context 'with user unverified email' do
- let(:email) { unverified_email.email }
+ context "when email param matches user's confirmed secondary email" do
+ let(:expected_user) { user }
+ let(:email) { user_confirmed_secondary_email }
- it_behaves_like 'does not send the password reset email'
+ it_behaves_like "sends 'Reset password instructions' email"
end
- context 'with one email matching user and one not matching' do
- let(:email) { [verified_email.email, 'other_email@example.com'] }
+ context "when email param matches user's unconfirmed secondary email" do
+ let(:email) { user_unconfirmed_secondary_email }
- it 'sends an email only to the user verified email' do
- expect { send_reset_password_instructions }
- .to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
- .with(
- user,
- anything, # reset token
- to: user.verified_emails(include_private_email: false)
- )
+ it_behaves_like "does not send 'Reset password instructions' email"
+ end
+
+ context 'when email param is unknown email' do
+ let(:email) { unknown_email }
+
+ it_behaves_like "does not send 'Reset password instructions' email"
+ end
+
+ context 'when email param is invalid email' do
+ let(:email) { invalid_email }
+
+ it_behaves_like "does not send 'Reset password instructions' email"
+ end
+
+ context 'when email param with attempt to cause SQL injection' do
+ let(:email) { sql_injection_email }
+
+ it_behaves_like "does not send 'Reset password instructions' email"
+ end
+
+ context 'when email param is nil' do
+ let(:email) { nil }
+
+ it_behaves_like "does not send 'Reset password instructions' email"
+ end
+
+ context 'when email param is empty string' do
+ let(:email) { '' }
+
+ it_behaves_like "does not send 'Reset password instructions' email"
+ end
+
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/436084
+ context 'when email param with multiple emails' do
+ let(:email) do
+ [
+ user_confirmed_primary_email,
+ user_confirmed_secondary_email,
+ user_unconfirmed_secondary_email,
+ unknown_email,
+ another_user_confirmed_primary_email,
+ another_user_unconfirmed_primary_email
+ ]
end
+
+ it_behaves_like "does not send 'Reset password instructions' email"
end
end
end
diff --git a/spec/models/integrations/mattermost_slash_commands_spec.rb b/spec/models/integrations/mattermost_slash_commands_spec.rb
index 3dee8737067..43316e164ed 100644
--- a/spec/models/integrations/mattermost_slash_commands_spec.rb
+++ b/spec/models/integrations/mattermost_slash_commands_spec.rb
@@ -125,5 +125,39 @@ RSpec.describe Integrations::MattermostSlashCommands, feature_category: :integra
end
end
end
+
+ describe '#redirect_url' do
+ let(:url) { 'http://www.mattermost.com/hooks' }
+
+ subject { integration.redirect_url('team', 'channel', url) }
+
+ it { is_expected.to eq("http://www.mattermost.com/team/channels/channel") }
+
+ context 'with invalid URL scheme' do
+ let(:url) { 'javascript://www.mattermost.com/hooks' }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with unsafe URL' do
+ let(:url) { "https://replaceme.com/'><script>alert(document.cookie)</script>" }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#confirmation_url' do
+ let(:params) do
+ {
+ team_domain: 'gitlab',
+ channel_name: 'test-channel',
+ response_url: 'http://mattermost.gitlab.com/hooks/commands/my123command'
+ }
+ end
+
+ subject { integration.confirmation_url('command-id', params) }
+
+ it { is_expected.to be_present }
+ end
end
end
diff --git a/spec/models/integrations/slack_slash_commands_spec.rb b/spec/models/integrations/slack_slash_commands_spec.rb
index f373fc2a2de..5d6f214a5d5 100644
--- a/spec/models/integrations/slack_slash_commands_spec.rb
+++ b/spec/models/integrations/slack_slash_commands_spec.rb
@@ -40,4 +40,27 @@ RSpec.describe Integrations::SlackSlashCommands, feature_category: :integrations
end
end
end
+
+ describe '#redirect_url' do
+ let(:integration) { build(:slack_slash_commands_integration) }
+
+ subject { integration.redirect_url('team', 'channel', 'www.example.com') }
+
+ it { is_expected.to eq('slack://channel?team=team&id=channel') }
+ end
+
+ describe '#confirmation_url' do
+ let(:integration) { build(:slack_slash_commands_integration) }
+ let(:params) do
+ {
+ team_id: 'T123456',
+ channel_id: 'C654321',
+ response_url: 'https://hooks.slack.com/services/T123456/C654321'
+ }
+ end
+
+ subject { integration.confirmation_url('command-id', params) }
+
+ it { is_expected.to be_present }
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 1c6a29f065f..b826475621a 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -6138,4 +6138,34 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
it { is_expected.to eq(false) }
end
end
+
+ describe '#previous_diff' do
+ let(:merge_request) { create(:merge_request, :skip_diff_creation) }
+
+ subject { merge_request.previous_diff }
+
+ context 'when there is are no merge_request_diffs' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there is one merge request_diff' do
+ let(:merge_request) { create(:merge_request) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there are multiple merge_request_diffs' do
+ let(:oldest_merge_request_diff) { create(:merge_request_diff, merge_request: merge_request) }
+ let(:second_to_last_merge_request_diff) { create(:merge_request_diff, merge_request: merge_request) }
+ let(:most_recent_merge_request_diff) { create(:merge_request_diff, merge_request: merge_request) }
+
+ before do
+ oldest_merge_request_diff
+ second_to_last_merge_request_diff
+ most_recent_merge_request_diff
+ end
+
+ it { is_expected.to eq(second_to_last_merge_request_diff) }
+ end
+ end
end
diff --git a/spec/requests/projects/integrations/slash_commands_controller_spec.rb b/spec/requests/projects/integrations/slash_commands_controller_spec.rb
new file mode 100644
index 00000000000..3d61f882bdf
--- /dev/null
+++ b/spec/requests/projects/integrations/slash_commands_controller_spec.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Projects::Integrations::SlashCommandsController, feature_category: :integrations do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, developer_projects: [project]) }
+ let_it_be(:chat_name) { create(:chat_name, user: user) }
+
+ let(:params) do
+ {
+ command_id: 'command-id',
+ integration: 'mattermost_slash_commands',
+ team: 1,
+ channel: 2,
+ response_url: 'http://www.example.com'
+ }
+ end
+
+ before do
+ create(:mattermost_slash_commands_integration, project: project)
+ end
+
+ describe 'GET #show' do
+ context 'when user is signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when request is invalid' do
+ it 'renders the "show" template with expired message' do
+ get project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(response.body).to include(
+ 'The slash command verification request has expired. Please run the command again.'
+ )
+ end
+ end
+
+ context 'when request is valid', :use_clean_rails_memory_store_caching do
+ before do
+ Rails.cache.write(
+ "slash-command-requests:#{params[:command_id]}", { team_id: chat_name.team_id, user_id: chat_name.chat_id }
+ )
+ stub_request(:post, "http://www.example.com/").to_return(status: 200, body: 'ok')
+ end
+
+ context 'when user is valid' do
+ it 'renders the "show" template with authorize button' do
+ get project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(response.body).to include('Authorize')
+ end
+ end
+
+ context 'when user is invalid' do
+ let(:chat_name) { create(:chat_name) }
+
+ it 'renders the "show" template' do
+ get project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(response.body).to include('The slash command request is invalid.')
+ end
+ end
+ end
+ end
+
+ context 'when user is not signed in' do
+ it 'redirects with a status of 302' do
+ get project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+ end
+
+ describe 'POST #confirm' do
+ let(:params) { super().merge(redirect_url: 'http://www.example.com') }
+
+ context 'when user is signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when request is invalid' do
+ it 'renders the "show" template' do
+ post confirm_project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(response.body).to include('The slash command request is invalid.')
+ end
+ end
+
+ context 'when request is valid', :use_clean_rails_memory_store_caching do
+ before do
+ Rails.cache.write(
+ "slash-command-requests:#{params[:command_id]}", { team_id: chat_name.team_id, user_id: chat_name.chat_id }
+ )
+ stub_request(:post, "http://www.example.com/").to_return(status: 200, body: 'ok')
+ end
+
+ context 'when user is valid' do
+ it 'redirects back to the integration' do
+ post confirm_project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+
+ context 'when user is invalid' do
+ let(:chat_name) { create(:chat_name) }
+
+ it 'renders the "show" template' do
+ post confirm_project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ expect(response.body).to include('The slash command request is invalid.')
+ end
+ end
+ end
+ end
+
+ context 'when user is not signed in' do
+ it 'redirects with a status of 302' do
+ post confirm_project_integrations_slash_commands_path(project), params: params
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb
index 9dffe035b2a..0bfcd88de4b 100644
--- a/spec/support/helpers/email_helpers.rb
+++ b/spec/support/helpers/email_helpers.rb
@@ -14,6 +14,18 @@ module EmailHelpers
ActiveJob::Base.queue_adapter.enqueued_jobs.clear
end
+ def expect_only_one_email_to_be_sent(subject:, to:)
+ count_of_sent_emails = ActionMailer::Base.deliveries.count
+ expect(count_of_sent_emails).to eq(1), "Expected only one email to be sent, but #{count_of_sent_emails} emails were sent instead"
+
+ return unless count_of_sent_emails == 1
+
+ message = ActionMailer::Base.deliveries.first
+
+ expect(message.subject).to eq(subject), "Expected '#{subject}' email to be sent, but '#{message.subject}' email was sent instead"
+ expect(message.to).to match_array(to), "Expected the email to be sent to #{to}, but it was sent to #{message.to} instead"
+ end
+
def should_only_email(*users, kind: :to)
recipients = email_recipients(kind: kind)
diff --git a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
index ff3cc1841b4..90bb75d402f 100644
--- a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
+++ b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
@@ -91,38 +91,72 @@ RSpec.shared_examples Integrations::BaseSlashCommands do
described_class.create!(project: project, properties: { token: 'token' })
end
- it 'triggers the command' do
- expect_any_instance_of(Gitlab::SlashCommands::Command).to receive(:execute)
+ context 'with verified request' do
+ before do
+ allow_next_instance_of(::Gitlab::SlashCommands::VerifyRequest) do |instance|
+ allow(instance).to receive(:valid?).and_return(true)
+ end
+ end
- subject.trigger(params)
- end
+ it 'triggers the command' do
+ expect_any_instance_of(Gitlab::SlashCommands::Command).to receive(:execute)
+
+ subject.trigger(params)
+ end
- shared_examples_for 'blocks command execution' do
- it do
- expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute)
+ shared_examples_for 'blocks command execution' do
+ it do
+ expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute)
- result = subject.trigger(params)
- expect(result[:text]).to match(error_message)
+ result = subject.trigger(params)
+ expect(result[:text]).to match(error_message)
+ end
end
- end
- context 'when user is blocked' do
- before do
- chat_name.user.block
+ context 'when user is blocked' do
+ before do
+ chat_name.user.block
+ end
+
+ it_behaves_like 'blocks command execution' do
+ let(:error_message) { 'you do not have access to the GitLab project' }
+ end
end
- it_behaves_like 'blocks command execution' do
- let(:error_message) { 'you do not have access to the GitLab project' }
+ context 'when user is deactivated' do
+ before do
+ chat_name.user.deactivate
+ end
+
+ it_behaves_like 'blocks command execution' do
+ let(:error_message) { "your #{Gitlab.config.gitlab.url} account needs to be reactivated" }
+ end
end
end
- context 'when user is deactivated' do
+ context 'with unverified request' do
before do
- chat_name.user.deactivate
+ allow_next_instance_of(::Gitlab::SlashCommands::VerifyRequest) do |instance|
+ allow(instance).to receive(:valid?).and_return(false)
+ end
end
- it_behaves_like 'blocks command execution' do
- let(:error_message) { "your #{Gitlab.config.gitlab.url} account needs to be reactivated" }
+ let(:params) do
+ {
+ team_domain: 'http://domain.tld',
+ channel_name: 'channel-test',
+ team_id: chat_name.team_id,
+ user_id: chat_name.chat_id,
+ response_url: 'http://domain.tld/commands',
+ token: 'token'
+ }
+ end
+
+ it 'caches the slash command params and returns confirmation message' do
+ expect(Rails.cache).to receive(:write).with(an_instance_of(String), params, { expires_in: 3.minutes })
+ expect_any_instance_of(Gitlab::SlashCommands::Presenters::Access).to receive(:confirm)
+
+ subject.trigger(params)
end
end
end