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
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2024-01-10 23:26:58 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-10 23:26:58 +0300
commitf57f7eebac215d23e6ca74d865bd19407cbaccba (patch)
tree047cb0a0e66bf9afc512ed2f02fdbe2d5d65978b /app
parent2965e48337030c75e342b72d3420b7ff69e11f08 (diff)
Add latest changes from gitlab-org/security/gitlab@16-7-stable-ee
Diffstat (limited to 'app')
-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
9 files changed, 193 insertions, 30 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..f477263303f 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
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
+ 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 bf21eca8857..f9af342f47f 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1159,6 +1159,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')