diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/integrations/apple_app_store.rb | 8 | ||||
-rw-r--r-- | app/models/integrations/gitlab_slack_application.rb | 176 | ||||
-rw-r--r-- | app/models/integrations/slack_workspace/api_scope.rb | 22 | ||||
-rw-r--r-- | app/models/integrations/slack_workspace/integration_api_scope.rb | 29 | ||||
-rw-r--r-- | app/models/merge_request.rb | 5 | ||||
-rw-r--r-- | app/models/resource_milestone_event.rb | 5 | ||||
-rw-r--r-- | app/models/resource_state_event.rb | 4 | ||||
-rw-r--r-- | app/models/slack_integration.rb | 93 | ||||
-rw-r--r-- | app/models/user.rb | 1 | ||||
-rw-r--r-- | app/models/user_detail.rb | 3 | ||||
-rw-r--r-- | app/models/work_items/resource_link_event.rb | 2 |
11 files changed, 337 insertions, 11 deletions
diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb index 9efc85cbdb1..5e502cce927 100644 --- a/app/models/integrations/apple_app_store.rb +++ b/app/models/integrations/apple_app_store.rb @@ -6,6 +6,7 @@ module Integrations class AppleAppStore < Integration ISSUER_ID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/.freeze KEY_ID_REGEX = /\A(?=.*[A-Z])(?=.*[0-9])[A-Z0-9]+\z/.freeze + IS_KEY_CONTENT_BASE64 = "true" SECTION_TYPE_APPLE_APP_STORE = 'apple_app_store' @@ -43,7 +44,8 @@ module Integrations variable_list = [ '<code>APP_STORE_CONNECT_API_KEY_ISSUER_ID</code>', '<code>APP_STORE_CONNECT_API_KEY_KEY_ID</code>', - '<code>APP_STORE_CONNECT_API_KEY_KEY</code>' + '<code>APP_STORE_CONNECT_API_KEY_KEY</code>', + '<code>APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64</code>' ] # rubocop:disable Layout/LineLength @@ -92,7 +94,9 @@ module Integrations { key: 'APP_STORE_CONNECT_API_KEY_ISSUER_ID', value: app_store_issuer_id, masked: true, public: false }, { key: 'APP_STORE_CONNECT_API_KEY_KEY', value: Base64.encode64(app_store_private_key), masked: true, public: false }, - { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: app_store_key_id, masked: true, public: false } + { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: app_store_key_id, masked: true, public: false }, + { key: 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64', value: IS_KEY_CONTENT_BASE64, masked: false, + public: false } ] end diff --git a/app/models/integrations/gitlab_slack_application.rb b/app/models/integrations/gitlab_slack_application.rb new file mode 100644 index 00000000000..b0f54f39e8c --- /dev/null +++ b/app/models/integrations/gitlab_slack_application.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +module Integrations + class GitlabSlackApplication < BaseSlackNotification + attribute :alert_events, default: false + attribute :commit_events, default: false + attribute :confidential_issues_events, default: false + attribute :confidential_note_events, default: false + attribute :deployment_events, default: false + attribute :issues_events, default: false + attribute :job_events, default: false + attribute :merge_requests_events, default: false + attribute :note_events, default: false + attribute :pipeline_events, default: false + attribute :push_events, default: false + attribute :tag_push_events, default: false + attribute :vulnerability_events, default: false + attribute :wiki_page_events, default: false + + has_one :slack_integration, foreign_key: :integration_id, inverse_of: :integration + delegate :bot_access_token, :bot_user_id, to: :slack_integration, allow_nil: true + + def update_active_status + update(active: !!slack_integration) + end + + def title + s_('Integrations|GitLab for Slack app') + end + + def description + s_('Integrations|Enable slash commands and notifications for a Slack workspace.') + end + + def self.to_param + 'gitlab_slack_application' + end + + override :show_active_box? + def show_active_box? + false + end + + override :test + def test(_data) + failures = test_notification_channels + + { success: failures.blank?, result: failures } + end + + # The form fields of this integration are editable only after the Slack App installation + # flow has been completed, which causes the integration to become activated/enabled. + override :editable? + def editable? + activated? + end + + override :fields + def fields + return [] unless editable? + + super + end + + override :sections + def sections + return [] unless editable? + + [ + { + type: SECTION_TYPE_TRIGGER, + title: s_('Integrations|Trigger'), + description: s_('Integrations|An event will be triggered when one of the following items happen.') + }, + { + type: SECTION_TYPE_CONFIGURATION, + title: s_('Integrations|Notification settings'), + description: s_('Integrations|Configure the scope of notifications.') + } + ] + end + + override :configurable_events + def configurable_events + return [] unless editable? + + super + end + + override :requires_webhook? + def requires_webhook? + false + end + + def upgrade_needed? + slack_integration.present? && slack_integration.upgrade_needed? + end + + private + + override :notify + def notify(message, opts) + channels = Array(opts[:channel]) + return false if channels.empty? + + payload = { + attachments: message.attachments, + text: message.pretext, + unfurl_links: false, + unfurl_media: false + } + + successes = channels.map do |channel| + notify_slack_channel!(channel, payload) + end + + successes.any? + end + + def notify_slack_channel!(channel, payload) + response = api_client.post( + 'chat.postMessage', + payload.merge(channel: channel) + ) + + log_error('Slack API error when notifying', api_response: response.parsed_response) unless response['ok'] + + response['ok'] + rescue *Gitlab::HTTP::HTTP_ERRORS => e + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, + { + integration_id: id, + slack_integration_id: slack_integration.id + } + ) + + false + end + + def api_client + @slack_api ||= ::Slack::API.new(slack_integration) + end + + def test_notification_channels + return if unique_channels.empty? + return s_('Integrations|GitLab for Slack app must be reinstalled to enable notifications') unless bot_access_token + + test_payload = { + text: 'Test', + user: bot_user_id + } + + not_found_channels = unique_channels.first(10).select do |channel| + test_payload[:channel] = channel + + response = ::Slack::API.new(slack_integration).post('chat.postEphemeral', test_payload) + response['error'] == 'channel_not_found' + end + + return if not_found_channels.empty? + + format( + s_( + 'Integrations|Unable to post to %{channel_list}, ' \ + 'please add the GitLab Slack app to any private Slack channels' + ), + channel_list: not_found_channels.to_sentence + ) + end + + override :metrics_key_prefix + def metrics_key_prefix + 'i_integrations_gitlab_for_slack_app' + end + end +end diff --git a/app/models/integrations/slack_workspace/api_scope.rb b/app/models/integrations/slack_workspace/api_scope.rb new file mode 100644 index 00000000000..3c4d25bff10 --- /dev/null +++ b/app/models/integrations/slack_workspace/api_scope.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Integrations + module SlackWorkspace + class ApiScope < ApplicationRecord + self.table_name = 'slack_api_scopes' + + def self.find_or_initialize_by_names(names) + found = where(name: names).to_a + missing_names = names - found.pluck(:name) + + if missing_names.any? + insert_all(missing_names.map { |name| { name: name } }) + missing = where(name: missing_names) + found += missing + end + + found + end + end + end +end diff --git a/app/models/integrations/slack_workspace/integration_api_scope.rb b/app/models/integrations/slack_workspace/integration_api_scope.rb new file mode 100644 index 00000000000..d33c8e0d816 --- /dev/null +++ b/app/models/integrations/slack_workspace/integration_api_scope.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Integrations + module SlackWorkspace + class IntegrationApiScope < ApplicationRecord + self.table_name = 'slack_integrations_scopes' + + belongs_to :slack_api_scope, class_name: 'Integrations::SlackWorkspace::ApiScope' + belongs_to :slack_integration + + # Efficient scope propagation + def self.update_scopes(integration_ids, scopes) + return if integration_ids.empty? + + scope_ids = scopes.pluck(:id) + + attrs = scope_ids.flat_map do |scope_id| + integration_ids.map { |si_id| { slack_integration_id: si_id, slack_api_scope_id: scope_id } } + end + + # We don't know which ones to preserve - so just delete them all in a single query + transaction do + where(slack_integration_id: integration_ids).delete_all + insert_all(attrs) unless attrs.empty? + end + end + end + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1e7ff6e8f0e..2d4c4daafe8 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -38,7 +38,10 @@ class MergeRequest < ApplicationRecord ALLOWED_TO_USE_MERGE_BASE_PIPELINE_FOR_COMPARISON = { 'Ci::CompareMetricsReportsService' => ->(project) { true }, - 'Ci::CompareCodequalityReportsService' => ->(project) { true } + 'Ci::CompareCodequalityReportsService' => ->(project) { true }, + 'Ci::CompareSecurityReportsService' => ->(project) do + Feature.enabled?(:use_merge_base_for_security_widget, project) + end }.freeze belongs_to :target_project, class_name: "Project" diff --git a/app/models/resource_milestone_event.rb b/app/models/resource_milestone_event.rb index 61129bbc9d8..d305a4ace51 100644 --- a/app/models/resource_milestone_event.rb +++ b/app/models/resource_milestone_event.rb @@ -4,9 +4,6 @@ class ResourceMilestoneEvent < ResourceTimeboxEvent belongs_to :milestone scope :include_relations, -> { includes(:user, milestone: [:project, :group]) } - scope :aliased_for_timebox_report, -> do - select("'timebox' AS event_type", "id", "created_at", "milestone_id AS value", "action", "issue_id") - end # state is used for issue and merge request states. enum state: Issue.available_states.merge(MergeRequest.available_states) @@ -23,3 +20,5 @@ class ResourceMilestoneEvent < ResourceTimeboxEvent MilestoneNote end end + +ResourceMilestoneEvent.prepend_mod diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb index e2ac762b1cd..134f71e35ad 100644 --- a/app/models/resource_state_event.rb +++ b/app/models/resource_state_event.rb @@ -13,10 +13,6 @@ class ResourceStateEvent < ResourceEvent after_create :issue_usage_metrics - scope :aliased_for_timebox_report, -> do - select("'state' AS event_type", "id", "created_at", "state AS value", "NULL AS action", "issue_id") - end - def self.issuable_attrs %i(issue merge_request).freeze end diff --git a/app/models/slack_integration.rb b/app/models/slack_integration.rb new file mode 100644 index 00000000000..22e911aeacd --- /dev/null +++ b/app/models/slack_integration.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +class SlackIntegration < ApplicationRecord + include EachBatch + + ALL_FEATURES = %i[commands notifications].freeze + + SCOPE_COMMANDS = 'commands' + SCOPE_CHAT_WRITE = 'chat:write' + SCOPE_CHAT_WRITE_PUBLIC = 'chat:write.public' + + # These scopes are requested when installing the app, additional scopes + # will need reauthorization. + # https://api.slack.com/authentication/oauth-v2#asking + SCOPES = [SCOPE_COMMANDS, SCOPE_CHAT_WRITE, SCOPE_CHAT_WRITE_PUBLIC].freeze + + belongs_to :integration + + attr_encrypted :bot_access_token, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_32, + algorithm: 'aes-256-gcm', + encode: false, + encode_iv: false + + has_many :slack_integrations_scopes, + class_name: '::Integrations::SlackWorkspace::IntegrationApiScope' + + has_many :slack_api_scopes, + class_name: '::Integrations::SlackWorkspace::ApiScope', + through: :slack_integrations_scopes + + scope :with_bot, -> { where.not(bot_user_id: nil) } + scope :by_team, ->(team_id) { where(team_id: team_id) } + + validates :team_id, presence: true + validates :team_name, presence: true + validates :alias, presence: true, + uniqueness: { scope: :team_id, message: 'This alias has already been taken' }, + length: 2..4096 + validates :user_id, presence: true + validates :integration, presence: true + + after_commit :update_active_status_of_integration, on: [:create, :destroy] + + def update_active_status_of_integration + integration.update_active_status + end + + def feature_available?(feature_name) + case feature_name + when :commands + # The slash commands feature requires 'commands' scope. + # All records will support this scope, as this was the original feature. + true + when :notifications + scoped_to?(SCOPE_CHAT_WRITE, SCOPE_CHAT_WRITE_PUBLIC) + else + false + end + end + + def upgrade_needed? + !all_features_supported? + end + + def all_features_supported? + ALL_FEATURES.all? { |feature| feature_available?(feature) } # rubocop: disable Gitlab/FeatureAvailableUsage + end + + def authorized_scope_names=(names) + names = Array.wrap(names).flat_map { |name| name.split(',') }.map(&:strip) + + scopes = ::Integrations::SlackWorkspace::ApiScope.find_or_initialize_by_names(names) + self.slack_api_scopes = scopes + end + + def authorized_scope_names + slack_api_scopes.pluck(:name) + end + + private + + def scoped_to?(*names) + return false if names.empty? + + names.to_set <= all_scopes + end + + def all_scopes + @all_scopes = authorized_scope_names.to_set + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 96223ac5027..0aa509e58d7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -380,7 +380,6 @@ class User < ApplicationRecord delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true - delegate :requires_credit_card_verification, :requires_credit_card_verification=, to: :user_detail, allow_nil: true delegate :linkedin, :linkedin=, to: :user_detail, allow_nil: true delegate :twitter, :twitter=, to: :user_detail, allow_nil: true delegate :skype, :skype=, to: :user_detail, allow_nil: true diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb index 9d3df3d6400..293a20fcc5a 100644 --- a/app/models/user_detail.rb +++ b/app/models/user_detail.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true class UserDetail < ApplicationRecord + include IgnorableColumns extend ::Gitlab::Utils::Override + ignore_column :requires_credit_card_verification, remove_with: '16.1', remove_after: '2023-06-22' + REGISTRATION_OBJECTIVE_PAIRS = { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5, joining_team: 6 }.freeze belongs_to :user diff --git a/app/models/work_items/resource_link_event.rb b/app/models/work_items/resource_link_event.rb index 64d51b2743c..6725acf8c68 100644 --- a/app/models/work_items/resource_link_event.rb +++ b/app/models/work_items/resource_link_event.rb @@ -12,3 +12,5 @@ module WorkItems } end end + +WorkItems::ResourceLinkEvent.prepend_mod |