diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
commit | 43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch) | |
tree | dceebdc68925362117480a5d672bcff122fb625b /app/models/integrations | |
parent | 20c84b99005abd1c82101dfeff264ac50d2df211 (diff) |
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'app/models/integrations')
19 files changed, 543 insertions, 90 deletions
diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb index 84185542939..5e502cce927 100644 --- a/app/models/integrations/apple_app_store.rb +++ b/app/models/integrations/apple_app_store.rb @@ -6,11 +6,15 @@ 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' with_options if: :activated? do validates :app_store_issuer_id, presence: true, format: { with: ISSUER_ID_REGEX } validates :app_store_key_id, presence: true, format: { with: KEY_ID_REGEX } validates :app_store_private_key, presence: true, certificate_key: true + validates :app_store_private_key_file_name, presence: true end field :app_store_issuer_id, @@ -21,15 +25,12 @@ module Integrations field :app_store_key_id, section: SECTION_TYPE_CONNECTION, required: true, - title: -> { s_('AppleAppStore|The Apple App Store Connect Key ID.') }, - is_secret: false + title: -> { s_('AppleAppStore|The Apple App Store Connect Key ID.') } - field :app_store_private_key, - section: SECTION_TYPE_CONNECTION, - required: true, - type: 'textarea', - title: -> { s_('AppleAppStore|The Apple App Store Connect Private Key.') }, - is_secret: false + field :app_store_private_key_file_name, + section: SECTION_TYPE_CONNECTION + + field :app_store_private_key, api_only: true def title 'Apple App Store Connect' @@ -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 @@ -51,7 +53,7 @@ module Integrations s_("Use the Apple App Store Connect integration to easily connect to the Apple App Store with Fastlane in CI/CD pipelines."), s_("After the Apple App Store Connect integration is activated, the following protected variables will be created for CI/CD use."), variable_list.join('<br>'), - s_(format("To get started, see the <a href='%{url}' target='_blank'>integration documentation</a> for instructions on how to generate App Store Connect credentials, and how to use this integration.", url: "https://docs.gitlab.com/ee/integration/apple_app_store.html")).html_safe + s_(format("To get started, see the <a href='%{url}' target='_blank'>integration documentation</a> for instructions on how to generate App Store Connect credentials, and how to use this integration.", url: Rails.application.routes.url_helpers.help_page_url('user/project/integrations/apple_app_store'))).html_safe ] # rubocop:enable Layout/LineLength @@ -69,7 +71,7 @@ module Integrations def sections [ { - type: SECTION_TYPE_CONNECTION, + type: SECTION_TYPE_APPLE_APP_STORE, title: s_('Integrations|Integration details'), description: help } @@ -92,20 +94,20 @@ 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 private def client - config = { + AppStoreConnect::Client.new( issuer_id: app_store_issuer_id, key_id: app_store_key_id, private_key: app_store_private_key - } - - AppStoreConnect::Client.new(config) + ) end end end diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb index fc5e6a88c2d..4638ca0c5f1 100644 --- a/app/models/integrations/bamboo.rb +++ b/app/models/integrations/bamboo.rb @@ -17,7 +17,8 @@ module Integrations non_empty_password_title: -> { s_('BambooService|Enter new build key') }, non_empty_password_help: -> { s_('BambooService|Leave blank to use your current build key.') }, placeholder: -> { _('KEY') }, - required: true + required: true, + is_secret: true field :username, help: -> { s_('BambooService|The user with API access to the Bamboo server.') } diff --git a/app/models/integrations/base_issue_tracker.rb b/app/models/integrations/base_issue_tracker.rb index e0994305e9d..7a54d354007 100644 --- a/app/models/integrations/base_issue_tracker.rb +++ b/app/models/integrations/base_issue_tracker.rb @@ -14,7 +14,7 @@ module Integrations # This pattern does not support cross-project references # The other code assumes that this pattern is a superset of all # overridden patterns. See ReferenceRegexes.external_pattern - def self.reference_pattern(only_long: false) + def self.base_reference_pattern(only_long: false) if only_long /(\b[A-Z][A-Z0-9_]*-)#{Gitlab::Regex.issue}/ else @@ -22,6 +22,10 @@ module Integrations end end + def reference_pattern(only_long: false) + self.class.base_reference_pattern(only_long: only_long) + end + def handle_properties # this has been moved from initialize_properties and should be improved # as part of https://gitlab.com/gitlab-org/gitlab/issues/29404 diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb index 7a2a91aa0d2..c83a559e0da 100644 --- a/app/models/integrations/base_slack_notification.rb +++ b/app/models/integrations/base_slack_notification.rb @@ -44,8 +44,6 @@ module Integrations Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id) - return unless Feature.enabled?(:route_hll_to_snowplow_phase2) - optional_arguments = { project: project, namespace: group || project&.namespace diff --git a/app/models/integrations/base_slash_commands.rb b/app/models/integrations/base_slash_commands.rb index 619579a543a..7662da933ba 100644 --- a/app/models/integrations/base_slash_commands.rb +++ b/app/models/integrations/base_slash_commands.rb @@ -6,10 +6,6 @@ module Integrations class BaseSlashCommands < Integration attribute :category, default: 'chat' - prop_accessor :token - - has_many :chat_names, foreign_key: :integration_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - def valid_token?(token) self.respond_to?(:token) && self.token.present? && @@ -24,18 +20,6 @@ module Integrations false end - def fields - [ - { - type: 'password', - name: 'token', - non_empty_password_title: s_('ProjectService|Enter new token'), - non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'), - placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' - } - ] - end - def trigger(params) return unless valid_token?(params[:token]) diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb index 3f7fa1c51b2..9b837faf79b 100644 --- a/app/models/integrations/campfire.rb +++ b/app/models/integrations/campfire.rb @@ -68,7 +68,7 @@ module Integrations def execute(data) return unless supported_events.include?(data[:object_kind]) - message = build_message(data) + message = create_message(data) speak(self.room, message, auth) end @@ -116,7 +116,7 @@ module Integrations res.code == 200 ? res["rooms"] : [] end - def build_message(push) + def create_message(push) ref = Gitlab::Git.ref_name(push[:ref]) before = push[:before] after = push[:after] diff --git a/app/models/integrations/ewm.rb b/app/models/integrations/ewm.rb index 1b86ef73c85..003c896704a 100644 --- a/app/models/integrations/ewm.rb +++ b/app/models/integrations/ewm.rb @@ -6,7 +6,7 @@ module Integrations validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? - def self.reference_pattern(only_long: true) + def reference_pattern(only_long: true) @reference_pattern ||= %r{(?<issue>\b(bug|task|work item|workitem|rtcwi|defect)\b\s+\d+)}i end diff --git a/app/models/integrations/field.rb b/app/models/integrations/field.rb index 329c046075f..9f2274216f6 100644 --- a/app/models/integrations/field.rb +++ b/app/models/integrations/field.rb @@ -2,8 +2,6 @@ module Integrations class Field - SECRET_NAME = %r/token|key|password|passphrase|secret/.freeze - BOOLEAN_ATTRIBUTES = %i[required api_only is_secret exposes_secrets].freeze ATTRIBUTES = %i[ @@ -17,11 +15,11 @@ module Integrations attr_reader :name, :integration_class - def initialize(name:, integration_class:, type: 'text', is_secret: true, api_only: false, **attributes) + def initialize(name:, integration_class:, type: 'text', is_secret: false, api_only: false, **attributes) @name = name.to_s.freeze @integration_class = integration_class - attributes[:type] = SECRET_NAME.match?(@name) && is_secret ? 'password' : type + attributes[:type] = is_secret ? 'password' : type attributes[:api_only] = api_only attributes[:is_secret] = is_secret @attributes = attributes.freeze 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/google_play.rb b/app/models/integrations/google_play.rb new file mode 100644 index 00000000000..9fa6dc19f11 --- /dev/null +++ b/app/models/integrations/google_play.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Integrations + class GooglePlay < Integration + PACKAGE_NAME_REGEX = /\A[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*){1,20}\z/ + + SECTION_TYPE_GOOGLE_PLAY = 'google_play' + + with_options if: :activated? do + validates :service_account_key, presence: true, json_schema: { + filename: "google_service_account_key", parse_json: true + } + validates :service_account_key_file_name, presence: true + validates :package_name, presence: true, format: { with: PACKAGE_NAME_REGEX } + end + + field :package_name, + section: SECTION_TYPE_CONNECTION, + placeholder: 'com.example.myapp', + required: true + + field :service_account_key_file_name, + section: SECTION_TYPE_CONNECTION, + required: true + + field :service_account_key, api_only: true + + def title + s_('GooglePlay|Google Play') + end + + def description + s_('GooglePlay|Use GitLab to build and release an app in Google Play.') + end + + def help + variable_list = [ + '<code>SUPPLY_PACKAGE_NAME</code>', + '<code>SUPPLY_JSON_KEY_DATA</code>' + ] + + # rubocop:disable Layout/LineLength + texts = [ + s_("Use the Google Play integration to connect to Google Play with fastlane in CI/CD pipelines."), + s_("After you enable the integration, the following protected variable is created for CI/CD use:"), + variable_list.join('<br>'), + s_(format("To generate a Google Play service account key and use this integration, see the <a href='%{url}' target='_blank'>integration documentation</a>.", url: Rails.application.routes.url_helpers.help_page_url('user/project/integrations/google_play'))).html_safe + ] + # rubocop:enable Layout/LineLength + + texts.join('<br><br>'.html_safe) + end + + def self.to_param + 'google_play' + end + + def self.supported_events + [] + end + + def sections + [ + { + type: SECTION_TYPE_GOOGLE_PLAY, + title: s_('Integrations|Integration details'), + description: help + } + ] + end + + def test(*_args) + client.list_reviews(package_name) + { success: true } + rescue Google::Apis::ClientError => error + { success: false, message: error } + end + + def ci_variables + return [] unless activated? + + [ + { key: 'SUPPLY_JSON_KEY_DATA', value: service_account_key, masked: true, public: false }, + { key: 'SUPPLY_PACKAGE_NAME', value: package_name, masked: false, public: false } + ] + end + + private + + def client + service = Google::Apis::AndroidpublisherV3::AndroidPublisherService.new # rubocop: disable CodeReuse/ServiceClass + + service.authorization = Google::Auth::ServiceAccountCredentials.make_creds( + json_key_io: StringIO.new(service_account_key), + scope: [Google::Apis::AndroidpublisherV3::AUTH_ANDROIDPUBLISHER] + ) + + service + end + end +end diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb index 01a04743d5d..079811e0df0 100644 --- a/app/models/integrations/harbor.rb +++ b/app/models/integrations/harbor.rb @@ -17,7 +17,8 @@ module Integrations field :project_name, title: -> { s_('HarborIntegration|Harbor project name') }, - help: -> { s_('HarborIntegration|The name of the project in Harbor.') } + help: -> { s_('HarborIntegration|The name of the project in Harbor.') }, + required: true field :username, title: -> { s_('HarborIntegration|Harbor username') }, @@ -62,7 +63,7 @@ module Integrations end def test(*_args) - client.ping + client.check_project_availability end def ci_variables diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb index d96a848c72e..2520d3bfc9c 100644 --- a/app/models/integrations/jira.rb +++ b/app/models/integrations/jira.rb @@ -17,12 +17,19 @@ module Integrations SECTION_TYPE_JIRA_TRIGGER = 'jira_trigger' SECTION_TYPE_JIRA_ISSUES = 'jira_issues' + AUTH_TYPE_BASIC = 0 + AUTH_TYPE_PAT = 1 + SNOWPLOW_EVENT_CATEGORY = self.name validates :url, public_url: true, presence: true, if: :activated? validates :api_url, public_url: true, allow_blank: true - validates :username, presence: true, if: :activated? + validates :username, presence: true, if: ->(object) { object.activated? && !object.personal_access_token_authorization? } validates :password, presence: true, if: :activated? + validates :jira_auth_type, presence: true, inclusion: { in: [AUTH_TYPE_BASIC, AUTH_TYPE_PAT] }, if: :activated? + validates :jira_issue_prefix, untrusted_regexp: true, length: { maximum: 255 }, if: :activated? + validates :jira_issue_regex, untrusted_regexp: true, length: { maximum: 255 }, if: :activated? + validate :validate_jira_cloud_auth_type_is_basic, if: :activated? validates :jira_issue_transition_id, format: { @@ -58,19 +65,44 @@ module Integrations help: -> { s_('JiraService|If different from the Web URL') }, exposes_secrets: true + field :jira_auth_type, + type: 'select', + required: true, + section: SECTION_TYPE_CONNECTION, + title: -> { s_('JiraService|Authentication type') }, + choices: -> { + [ + [s_('JiraService|Basic'), AUTH_TYPE_BASIC], + [s_('JiraService|Jira personal access token (Jira Data Center and Jira Server only)'), AUTH_TYPE_PAT] + ] + } + field :username, section: SECTION_TYPE_CONNECTION, - required: true, - title: -> { s_('JiraService|Username or email') }, - help: -> { s_('JiraService|Username for the server version or an email for the cloud version') } + required: false, + title: -> { s_('JiraService|Email or username') }, + help: -> { s_('JiraService|Only required for Basic authentication. Email for Jira Cloud or username for Jira Data Center and Jira Server') } field :password, section: SECTION_TYPE_CONNECTION, required: true, title: -> { s_('JiraService|Password or API token') }, - non_empty_password_title: -> { s_('JiraService|Enter new password or API token') }, - non_empty_password_help: -> { s_('JiraService|Leave blank to use your current password or API token.') }, - help: -> { s_('JiraService|Password for the server version or an API token for the cloud version') } + non_empty_password_title: -> { s_('JiraService|New API token, password, or Jira personal access token') }, + non_empty_password_help: -> { s_('JiraService|Leave blank to use your current configuration') }, + help: -> { s_('JiraService|API token for Jira Cloud or password for Jira Data Center and Jira Server') }, + is_secret: true + + field :jira_issue_regex, + section: SECTION_TYPE_CONFIGURATION, + required: false, + title: -> { s_('JiraService|Jira issue regex') }, + help: -> { s_('JiraService|Use regular expression to match Jira issue keys.') } + + field :jira_issue_prefix, + section: SECTION_TYPE_CONFIGURATION, + required: false, + title: -> { s_('JiraService|Jira issue prefix') }, + help: -> { s_('JiraService|Use a prefix to match Jira issue keys.') } field :jira_issue_transition_id, api_only: true @@ -90,8 +122,8 @@ module Integrations end # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 - def self.reference_pattern(only_long: true) - @reference_pattern ||= /(?<issue>\b#{Gitlab::Regex.jira_issue_key_regex})/ + def reference_pattern(only_long: true) + @reference_pattern ||= jira_issue_match_regex end def self.valid_jira_cloud_url?(url) @@ -119,16 +151,23 @@ module Integrations def options url = URI.parse(client_url) - { - username: username&.strip, - password: password, - site: URI.join(url, '/').to_s.delete_suffix('/'), # Intended to find the root + options = { + site: URI.join(url, '/').to_s.chomp('/'), # Find the root URL context_path: (url.path.presence || '/').delete_suffix('/'), auth_type: :basic, - use_cookies: true, - additional_cookies: ['OBBasicAuth=fromDialog'], use_ssl: url.scheme == 'https' } + + if personal_access_token_authorization? + options[:default_headers] = { 'Authorization' => "Bearer #{password}" } + else + options[:username] = username&.strip + options[:password] = password + options[:use_cookies] = true + options[:additional_cookies] = ['OBBasicAuth=fromDialog'] + end + + options end def client @@ -166,6 +205,11 @@ module Integrations type: SECTION_TYPE_JIRA_TRIGGER, title: _('Trigger'), description: s_('JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link and comment (if enabled) will be created.') + }, + { + type: SECTION_TYPE_CONFIGURATION, + title: _('Jira issue matching'), + description: s_('Configure custom rules for Jira issue key matching') } ] @@ -323,8 +367,18 @@ module Integrations jira_issue_transition_automatic || jira_issue_transition_id.present? end + def personal_access_token_authorization? + jira_auth_type == AUTH_TYPE_PAT + end + private + def jira_issue_match_regex + match_regex = (jira_issue_regex.presence || Gitlab::Regex.jira_issue_key_regex) + + /\b#{jira_issue_prefix}(?<issue>#{match_regex})/ + end + def parse_project_from_issue_key(issue_key) issue_key.gsub(Gitlab::Regex.jira_issue_key_project_key_extraction_regex, '') end @@ -391,8 +445,6 @@ module Integrations Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user.id) - return unless Feature.enabled?(:route_hll_to_snowplow_phase2) - optional_arguments = { project: project, namespace: group || project&.namespace @@ -606,7 +658,6 @@ module Integrations # If API-based detection methods fail here then # we can only assume it's either Cloud or Server # based on the URL being *.atlassian.net - if self.class.valid_jira_cloud_url?(client_url) data_fields.deployment_cloud! else @@ -626,6 +677,17 @@ module Integrations description end + + def validate_jira_cloud_auth_type_is_basic + return unless self.class.valid_jira_cloud_url?(client_url) && jira_auth_type != AUTH_TYPE_BASIC + + errors.add(:base, + format( + s_('JiraService|For Jira Cloud, the authentication type must be %{basic}'), + basic: s_('JiraService|Basic') + ) + ) + end end end diff --git a/app/models/integrations/mattermost_slash_commands.rb b/app/models/integrations/mattermost_slash_commands.rb index 30a8ba973c1..e075400d9b5 100644 --- a/app/models/integrations/mattermost_slash_commands.rb +++ b/app/models/integrations/mattermost_slash_commands.rb @@ -4,18 +4,22 @@ module Integrations class MattermostSlashCommands < BaseSlashCommands include Ci::TriggersHelper - prop_accessor :token + field :token, + type: 'password', + non_empty_password_title: -> { s_('ProjectService|Enter new token') }, + non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, + placeholder: '' def testable? false end def title - 'Mattermost slash commands' + s_('Integrations|Mattermost slash commands') end def description - "Perform common tasks with slash commands." + s_('Integrations|Perform common tasks with slash commands.') end def self.to_param @@ -37,10 +41,6 @@ module Integrations [[], e.message] end - def chat_responder - ::Gitlab::Chat::Responder::Mattermost - end - private def command(params) diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb index 2f0995e9ab0..2dc0fd7d011 100644 --- a/app/models/integrations/prometheus.rb +++ b/app/models/integrations/prometheus.rb @@ -30,12 +30,9 @@ module Integrations help: -> { s_('PrometheusService|The contents of the credentials.json file of your service account.') }, required: false - # We need to allow the self-monitoring project to connect to the internal - # Prometheus instance. # Since the internal Prometheus instance is usually a localhost URL, we need # to allow localhost URLs when the following conditions are true: - # 1. project is the self-monitoring project. - # 2. api_url is the internal Prometheus URL. + # 1. api_url is the internal Prometheus URL. with_options presence: true do validates :api_url, public_url: true, if: ->(object) { object.manual_configuration? && !object.allow_local_api_url? } validates :api_url, url: true, if: ->(object) { object.manual_configuration? && object.allow_local_api_url? } @@ -99,8 +96,7 @@ module Integrations end def allow_local_api_url? - allow_local_requests_from_web_hooks_and_services? || - (self_monitoring_project? && internal_prometheus_url?) + allow_local_requests_from_web_hooks_and_services? || internal_prometheus_url? end def configured? @@ -127,10 +123,6 @@ module Integrations delegate :allow_local_requests_from_web_hooks_and_services?, to: :current_settings, private: true - def self_monitoring_project? - project && project.id == current_settings.self_monitoring_project_id - end - def internal_prometheus_url? api_url.present? && api_url == ::Gitlab::Prometheus::Internal.uri end diff --git a/app/models/integrations/slack_slash_commands.rb b/app/models/integrations/slack_slash_commands.rb index 72e3c4a8cbc..343c8d68166 100644 --- a/app/models/integrations/slack_slash_commands.rb +++ b/app/models/integrations/slack_slash_commands.rb @@ -4,6 +4,12 @@ module Integrations class SlackSlashCommands < BaseSlashCommands include Ci::TriggersHelper + field :token, + type: 'password', + non_empty_password_title: -> { s_('ProjectService|Enter new token') }, + non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, + placeholder: '' + def title 'Slack slash commands' end @@ -23,10 +29,6 @@ module Integrations end end - def chat_responder - ::Gitlab::Chat::Responder::Slack - end - private def format(text) 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/integrations/squash_tm.rb b/app/models/integrations/squash_tm.rb new file mode 100644 index 00000000000..e0a63b5ae6a --- /dev/null +++ b/app/models/integrations/squash_tm.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module Integrations + class SquashTm < Integration + include HasWebHook + + field :url, + placeholder: 'https://your-instance.squashcloud.io/squash/plugin/xsquash4gitlab/webhook/issue', + title: -> { s_('SquashTmIntegration|Squash TM webhook URL') }, + exposes_secrets: true, + required: true + + field :token, + type: 'password', + title: -> { s_('SquashTmIntegration|Secret token (optional)') }, + non_empty_password_title: -> { s_('ProjectService|Enter new token') }, + non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, + required: false + + with_options if: :activated? do + validates :url, presence: true, public_url: true + validates :token, length: { maximum: 255 }, allow_blank: true + end + + def title + 'Squash TM' + end + + def description + s_("SquashTmIntegration|Update Squash TM requirements when GitLab issues are modified.") + end + + def help + docs_link = ActionController::Base.helpers.link_to( + _('Learn more.'), + Rails.application.routes.url_helpers.help_page_url('user/project/integrations/squash_tm'), + target: '_blank', + rel: 'noopener noreferrer' + ) + + Kernel.format( + s_('SquashTmIntegration|Update Squash TM requirements when GitLab issues are modified. %{docs_link}'), + { docs_link: docs_link.html_safe } + ).html_safe + end + + def self.supported_events + %w[issue confidential_issue] + end + + def self.to_param + 'squash_tm' + end + + def self.default_test_event + 'issue' + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + execute_web_hook!(data, "#{data[:object_kind]} Hook") + end + + def test(data) + result = execute_web_hook!(data, "Test Configuration Hook") + + { success: result.payload[:http_status] == 200, result: result.message } + rescue StandardError => error + { success: false, result: error.message } + end + + override :hook_url + def hook_url + format("#{url}%s", ('?token={token}' unless token.blank?)) + end + + def url_variables + { 'token' => token }.compact + end + end +end diff --git a/app/models/integrations/youtrack.rb b/app/models/integrations/youtrack.rb index fa719f925ed..15246a37aa7 100644 --- a/app/models/integrations/youtrack.rb +++ b/app/models/integrations/youtrack.rb @@ -7,12 +7,11 @@ module Integrations validates :project_url, :issues_url, presence: true, public_url: true, if: :activated? # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030 - def self.reference_pattern(only_long: false) - if only_long - /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)/ - else - /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)|(#{Issue.reference_prefix}#{Gitlab::Regex.issue})/ - end + def reference_pattern(only_long: false) + return @reference_pattern if defined?(@reference_pattern) + + regex_suffix = "|(#{Issue.reference_prefix}#{Gitlab::Regex.issue})" + @reference_pattern = /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)#{regex_suffix if only_long}/ end def title |