summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/integrations/apple_app_store.rb8
-rw-r--r--app/models/integrations/gitlab_slack_application.rb176
-rw-r--r--app/models/integrations/slack_workspace/api_scope.rb22
-rw-r--r--app/models/integrations/slack_workspace/integration_api_scope.rb29
-rw-r--r--app/models/merge_request.rb5
-rw-r--r--app/models/resource_milestone_event.rb5
-rw-r--r--app/models/resource_state_event.rb4
-rw-r--r--app/models/slack_integration.rb93
-rw-r--r--app/models/user.rb1
-rw-r--r--app/models/user_detail.rb3
-rw-r--r--app/models/work_items/resource_link_event.rb2
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