diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-10 23:26:58 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-10 23:26:58 +0300 |
commit | f57f7eebac215d23e6ca74d865bd19407cbaccba (patch) | |
tree | 047cb0a0e66bf9afc512ed2f02fdbe2d5d65978b /app | |
parent | 2965e48337030c75e342b72d3420b7ff69e11f08 (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.rb | 81 | ||||
-rw-r--r-- | app/models/chat_name.rb | 7 | ||||
-rw-r--r-- | app/models/concerns/recoverable_by_any_email.rb | 27 | ||||
-rw-r--r-- | app/models/integrations/base_slash_commands.rb | 41 | ||||
-rw-r--r-- | app/models/integrations/mattermost_slash_commands.rb | 17 | ||||
-rw-r--r-- | app/models/integrations/slack_slash_commands.rb | 14 | ||||
-rw-r--r-- | app/models/merge_request.rb | 4 | ||||
-rw-r--r-- | app/views/devise/passwords/new.html.haml | 2 | ||||
-rw-r--r-- | app/views/projects/integrations/slash_commands/show.html.haml | 30 |
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') |