Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-05-01 00:10:00 +00:00
parent 07de482288
commit 4601b44af1
19 changed files with 415 additions and 94 deletions

View File

@ -477,6 +477,9 @@ group :ed25519 do
gem 'bcrypt_pbkdf', '~> 1.0' gem 'bcrypt_pbkdf', '~> 1.0'
end end
# Spamcheck GRPC protocol definitions
gem 'spamcheck', '~> 0.0.5'
# Gitaly GRPC protocol definitions # Gitaly GRPC protocol definitions
gem 'gitaly', '~> 13.11.0.pre.rc1' gem 'gitaly', '~> 13.11.0.pre.rc1'

View File

@ -1211,6 +1211,8 @@ GEM
thor (~> 1.0) thor (~> 1.0)
tilt (~> 2.0) tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24) yard (~> 0.9, >= 0.9.24)
spamcheck (0.0.5)
grpc (~> 1.0)
spring (2.1.1) spring (2.1.1)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
spring (>= 0.9.1) spring (>= 0.9.1)
@ -1606,6 +1608,7 @@ DEPENDENCIES
slack-messenger (~> 2.3.4) slack-messenger (~> 2.3.4)
snowplow-tracker (~> 0.6.1) snowplow-tracker (~> 0.6.1)
solargraph (~> 0.40.4) solargraph (~> 0.40.4)
spamcheck (~> 0.0.5)
spring (~> 2.1.0) spring (~> 2.1.0)
spring-commands-rspec (~> 1.0.4) spring-commands-rspec (~> 1.0.4)
sprockets (~> 3.7.0) sprockets (~> 3.7.0)

View File

@ -375,7 +375,7 @@ class ApplicationSetting < ApplicationRecord
if: :external_authorization_service_enabled if: :external_authorization_service_enabled
validates :spam_check_endpoint_url, validates :spam_check_endpoint_url,
addressable_url: true, allow_blank: true addressable_url: { schemes: %w(grpc) }, allow_blank: true
validates :spam_check_endpoint_url, validates :spam_check_endpoint_url,
presence: true, presence: true,
@ -524,7 +524,7 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :lets_encrypt_private_key, encryption_options_base_32_aes_256_gcm attr_encrypted :lets_encrypt_private_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :eks_secret_access_key, encryption_options_base_32_aes_256_gcm attr_encrypted :eks_secret_access_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :akismet_api_key, encryption_options_base_32_aes_256_gcm attr_encrypted :akismet_api_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :spam_check_api_key, encryption_options_base_32_aes_256_gcm attr_encrypted :spam_check_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false)
attr_encrypted :elasticsearch_aws_secret_access_key, encryption_options_base_32_aes_256_gcm attr_encrypted :elasticsearch_aws_secret_access_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :recaptcha_private_key, encryption_options_base_32_aes_256_gcm attr_encrypted :recaptcha_private_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :recaptcha_site_key, encryption_options_base_32_aes_256_gcm attr_encrypted :recaptcha_site_key, encryption_options_base_32_aes_256_gcm

View File

@ -20,7 +20,7 @@ module Spam
created_at: DateTime.current, created_at: DateTime.current,
author: owner_name, author: owner_name,
author_email: owner_email, author_email: owner_email,
referrer: options[:referrer] referer: options[:referer]
} }
begin begin

View File

@ -53,7 +53,7 @@ module Spam
if request if request
options[:ip_address] = request.env['action_dispatch.remote_ip'].to_s options[:ip_address] = request.env['action_dispatch.remote_ip'].to_s
options[:user_agent] = request.env['HTTP_USER_AGENT'] options[:user_agent] = request.env['HTTP_USER_AGENT']
options[:referrer] = request.env['HTTP_REFERRER'] options[:referer] = request.env['HTTP_REFERER']
else else
# TODO: This code is never used, because we do not perform a verification if there is not a # TODO: This code is never used, because we do not perform a verification if there is not a
# request. Why? Should it be deleted? Or should we check even if there is no request? # request. Why? Should it be deleted? Or should we check even if there is no request?
@ -123,6 +123,11 @@ module Spam
# https://gitlab.com/gitlab-org/gitlab/-/issues/214739 # https://gitlab.com/gitlab-org/gitlab/-/issues/214739
target.spam! unless target.allow_possible_spam? target.spam! unless target.allow_possible_spam?
create_spam_log(api) create_spam_log(api)
when BLOCK_USER
# TODO: improve BLOCK_USER handling, non-existent until now
# https://gitlab.com/gitlab-org/gitlab/-/issues/329666
target.spam! unless target.allow_possible_spam?
create_spam_log(api)
when ALLOW when ALLOW
target.clear_spam_flags! target.clear_spam_flags!
end end

View File

@ -10,25 +10,44 @@ module Spam
@request = request @request = request
@user = user @user = user
@options = options @options = options
@verdict_params = assemble_verdict_params(context) @context = context
end end
def execute def execute
external_spam_check_result = external_verdict spamcheck_result = nil
external_spam_check_round_trip_time = Benchmark.realtime do
spamcheck_result = spamcheck_verdict
end
akismet_result = akismet_verdict akismet_result = akismet_verdict
# filter out anything we don't recognise, including nils. # filter out anything we don't recognise, including nils.
valid_results = [external_spam_check_result, akismet_result].compact.select { |r| SUPPORTED_VERDICTS.key?(r) } valid_results = [spamcheck_result, akismet_result].compact.select { |r| SUPPORTED_VERDICTS.key?(r) }
# Treat nils - such as service unavailable - as ALLOW # Treat nils - such as service unavailable - as ALLOW
return ALLOW unless valid_results.any? return ALLOW unless valid_results.any?
# Favour the most restrictive result. # Favour the most restrictive result.
valid_results.min_by { |v| SUPPORTED_VERDICTS[v][:priority] } final_verdict = valid_results.min_by { |v| SUPPORTED_VERDICTS[v][:priority] }
logger.info(class: self.class.name,
akismet_verdict: akismet_verdict,
spam_check_verdict: spamcheck_result,
spam_check_rtt: external_spam_check_round_trip_time.real,
final_verdict: final_verdict,
username: user.username,
user_id: user.id,
target_type: target.class.to_s,
project_id: target.project_id
)
final_verdict
end end
private private
attr_reader :user, :target, :request, :options, :verdict_params attr_reader :user, :target, :request, :options, :context
def akismet_verdict def akismet_verdict
if akismet.spam? if akismet.spam?
@ -38,54 +57,34 @@ module Spam
end end
end end
def external_verdict def spamcheck_verdict
return unless Gitlab::CurrentSettings.spam_check_endpoint_enabled return unless Gitlab::CurrentSettings.spam_check_endpoint_enabled
return if endpoint_url.blank?
begin begin
result = Gitlab::HTTP.post(endpoint_url, body: verdict_params.to_json, headers: { 'Content-Type' => 'application/json' }) result, _error = spamcheck_client.issue_spam?(spam_issue: target, user: user, context: context)
return unless result return unless result
json_result = Gitlab::Json.parse(result).with_indifferent_access # @TODO log if error is not nil https://gitlab.com/gitlab-org/gitlab/-/issues/329545
# @TODO metrics/logging
# Expecting:
# error: (string or nil)
# verdict: (string or nil)
# @TODO log if json_result[:error]
json_result[:verdict] # Duplicate logic with Akismet logic in #akismet_verdict
rescue *Gitlab::HTTP::HTTP_ERRORS => e if Gitlab::Recaptcha.enabled? && result != ALLOW
# @TODO: log error via try_post https://gitlab.com/gitlab-org/gitlab/-/issues/219223 CONDITIONAL_ALLOW
else
result
end
rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e) Gitlab::ErrorTracking.log_exception(e)
nil # Default to ALLOW if any errors occur
rescue StandardError
# @TODO log
ALLOW ALLOW
end end
end end
def assemble_verdict_params(context) def spamcheck_client
return {} unless endpoint_url.present? @spamcheck_client ||= Gitlab::Spamcheck::Client.new
project = target.try(:project)
context.merge({
target: {
title: target.spam_title,
description: target.spam_description,
type: target.class.to_s
},
user: {
created_at: user.created_at,
email: user.email,
username: user.username
},
user_in_project: user.authorized_project?(project)
})
end end
def endpoint_url def logger
@endpoint_url ||= Gitlab::CurrentSettings.current_application_settings.spam_check_endpoint_url @logger ||= Gitlab::AppJsonLogger.build
end end
end end
end end

View File

@ -51,12 +51,11 @@
= render 'new_project_fields', f: f, project_name_id: "blank-project-name" = render 'new_project_fields', f: f, project_name_id: "blank-project-name"
#create-from-template-pane.tab-pane.js-toggle-container.px-0.pb-0{ class: active_when(active_tab == 'template'), role: 'tabpanel' } #create-from-template-pane.tab-pane.js-toggle-container.px-0.pb-0{ class: active_when(active_tab == 'template'), role: 'tabpanel' }
.gl-card.gl-my-5 .card.card-slim.m-4.p-4
.gl-card-body %div
%div - contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
- contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing' - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url }
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url } = _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= form_for @project, html: { class: 'new_project' } do |f| = form_for @project, html: { class: 'new_project' } do |f|
.project-template .project-template
.form-group .form-group

View File

@ -0,0 +1,5 @@
---
title: Integrate with the Spamcheck anti-spam engine
merge_request: 52385
author:
type: added

View File

@ -1,5 +0,0 @@
---
title: Remove template tabs in new project page for GitLab.com
merge_request: 59083
author: Yogi (@yo)
type: changed

View File

@ -256,6 +256,30 @@ These route prefixes guarantee a higher rate limit:
/api/v4/groups/:group_id/-/packages/ /api/v4/groups/:group_id/-/packages/
``` ```
### MVC Checklist
When adding support to GitLab for a new package manager, the first iteration must contain the
following features. You can add the features through many merge requests as needed, but all the
features must be implemented when the feature flag is removed.
- Project-level API
- Push event tracking
- Pull event tracking
- Authentication with Personal Access Tokens
- Authentication with Job Tokens
- Authentication with Deploy Tokens (group and project)
- File size [limit](#file-size-limits)
- File format guards (only accept valid file formats for the package type)
- Name regex with validation
- Version regex with validation
- Workhorse route for [accelerated](uploads.md#how-to-add-a-new-upload-route) uploads
- Background workers for extracting package metadata (if applicable)
- Documentation (how to use the feature)
- API Documentation (individual endpoints with curl examples)
- Seeding in [`db/fixtures/development/26_packages.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/db/fixtures/development/26_packages.rb)
- Update the [runbook](https://gitlab.com/gitlab-com/runbooks/-/blob/31fb4959e89db25fddf865bc81734c222daf32dd/dashboards/stage-groups/package.dashboard.jsonnet#L74) for the Grafana charts
- End-to-end feature tests for (at the minimum) publishing and installing a package
### Future Work ### Future Work
While working on the MVC, contributors might find features that are not mandatory for the MVC but can provide a better user experience. It's generally a good idea to keep an eye on those and open issues. While working on the MVC, contributors might find features that are not mandatory for the MVC but can provide a better user experience. It's generally a good idea to keep an eye on those and open issues.

View File

@ -109,6 +109,14 @@ on an existing group's page.
## Version history ## Version history
### 14.0+
In GitLab 14.0, the JSON format is no longer supported for project and group exports. To allow for a
transitional period, you can still import any JSON exports. The new format for imports and exports
is NDJSON.
### 13.0+
GitLab can import bundles that were exported from a different GitLab deployment. GitLab can import bundles that were exported from a different GitLab deployment.
This ability is limited to two previous GitLab [minor](../../../policy/maintenance.md#versioning) This ability is limited to two previous GitLab [minor](../../../policy/maintenance.md#versioning)
releases, which is similar to our process for [Security Releases](../../../policy/maintenance.md#security-releases). releases, which is similar to our process for [Security Releases](../../../policy/maintenance.md#security-releases).

View File

@ -57,6 +57,12 @@ Note the following:
## Version history ## Version history
### 14.0+
In GitLab 14.0, the JSON format is no longer supported for project and group exports. To allow for a
transitional period, you can still import any JSON exports. The new format for imports and exports
is NDJSON.
### 13.0+ ### 13.0+
Starting with GitLab 13.0, GitLab can import bundles that were exported from a different GitLab deployment. Starting with GitLab 13.0, GitLab can import bundles that were exported from a different GitLab deployment.

View File

@ -0,0 +1,96 @@
# frozen_string_literal: true
require 'spamcheck'
module Gitlab
module Spamcheck
class Client
include ::Spam::SpamConstants
DEFAULT_TIMEOUT_SECS = 2
VERDICT_MAPPING = {
::Spamcheck::SpamVerdict::Verdict::ALLOW => ALLOW,
::Spamcheck::SpamVerdict::Verdict::CONDITIONAL_ALLOW => CONDITIONAL_ALLOW,
::Spamcheck::SpamVerdict::Verdict::DISALLOW => DISALLOW,
::Spamcheck::SpamVerdict::Verdict::BLOCK => BLOCK_USER
}.freeze
ACTION_MAPPING = {
create: ::Spamcheck::Action::CREATE,
update: ::Spamcheck::Action::UPDATE
}.freeze
def initialize
@endpoint_url = Gitlab::CurrentSettings.current_application_settings.spam_check_endpoint_url
# remove the `grpc://` as it's only useful to ensure we're expecting to
# connect with Spamcheck
@endpoint_url = @endpoint_url.gsub(%r(^grpc:\/\/), '')
creds =
if Rails.env.development? || Rails.env.test?
:this_channel_is_insecure
else
GRPC::Core::ChannelCredentials.new
end
@stub = ::Spamcheck::SpamcheckService::Stub.new(@endpoint_url, creds,
timeout: DEFAULT_TIMEOUT_SECS)
end
def issue_spam?(spam_issue:, user:, context: {})
issue = build_issue_protobuf(issue: spam_issue, user: user, context: context)
response = @stub.check_for_spam_issue(issue,
metadata: { 'authorization' =>
Gitlab::CurrentSettings.spam_check_api_key })
verdict = convert_verdict_to_gitlab_constant(response.verdict)
[verdict, response.error]
end
private
def convert_verdict_to_gitlab_constant(verdict)
VERDICT_MAPPING.fetch(::Spamcheck::SpamVerdict::Verdict.resolve(verdict), verdict)
end
def build_issue_protobuf(issue:, user:, context:)
issue_pb = ::Spamcheck::Issue.new
issue_pb.title = issue.spam_title || ''
issue_pb.description = issue.spam_description || ''
issue_pb.created_at = convert_to_pb_timestamp(issue.created_at) if issue.created_at
issue_pb.updated_at = convert_to_pb_timestamp(issue.updated_at) if issue.updated_at
issue_pb.user_in_project = user.authorized_project?(issue.project)
issue_pb.action = ACTION_MAPPING.fetch(context.fetch(:action)) if context.has_key?(:action)
issue_pb.user = build_user_protobuf(user)
issue_pb
end
def build_user_protobuf(user)
user_pb = ::Spamcheck::User.new
user_pb.username = user.username
user_pb.org = user.organization || ''
user_pb.created_at = convert_to_pb_timestamp(user.created_at)
user_pb.emails << build_email(user.email, user.confirmed?)
user.emails.each do |email|
user_pb.emails << build_email(email.email, email.confirmed?)
end
user_pb
end
def build_email(email, verified)
email_pb = ::Spamcheck::User::Email.new
email_pb.email = email
email_pb.verified = verified
email_pb
end
def convert_to_pb_timestamp(ar_timestamp)
Google::Protobuf::Timestamp.new(seconds: ar_timestamp.to_time.to_i,
nanos: ar_timestamp.to_time.nsec)
end
end
end
end

View File

@ -425,7 +425,7 @@ RSpec.describe 'Admin updates settings' do
check 'Enable reCAPTCHA for login' check 'Enable reCAPTCHA for login'
fill_in 'IPs per user', with: 15 fill_in 'IPs per user', with: 15
check 'Enable Spam Check via external API endpoint' check 'Enable Spam Check via external API endpoint'
fill_in 'URL of the external Spam Check endpoint', with: 'https://www.example.com/spamcheck' fill_in 'URL of the external Spam Check endpoint', with: 'grpc://www.example.com/spamcheck'
fill_in 'Spam Check API Key', with: 'SPAM_CHECK_API_KEY' fill_in 'Spam Check API Key', with: 'SPAM_CHECK_API_KEY'
click_button 'Save changes' click_button 'Save changes'
end end
@ -435,7 +435,7 @@ RSpec.describe 'Admin updates settings' do
expect(current_settings.login_recaptcha_protection_enabled).to be true expect(current_settings.login_recaptcha_protection_enabled).to be true
expect(current_settings.unique_ips_limit_per_user).to eq(15) expect(current_settings.unique_ips_limit_per_user).to eq(15)
expect(current_settings.spam_check_endpoint_enabled).to be true expect(current_settings.spam_check_endpoint_enabled).to be true
expect(current_settings.spam_check_endpoint_url).to eq 'https://www.example.com/spamcheck' expect(current_settings.spam_check_endpoint_url).to eq 'grpc://www.example.com/spamcheck'
end end
end end

View File

@ -0,0 +1,103 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Spamcheck::Client do
include_context 'includes Spam constants'
let(:endpoint) { 'grpc://grpc.test.url' }
let_it_be(:user) { create(:user, organization: 'GitLab') }
let(:verdict_value) { nil }
let(:error_value) { "" }
let_it_be(:issue) { create(:issue, description: 'Test issue description') }
let(:response) do
verdict = ::Spamcheck::SpamVerdict.new
verdict.verdict = verdict_value
verdict.error = error_value
verdict
end
subject { described_class.new.issue_spam?(spam_issue: issue, user: user) }
before do
stub_application_setting(spam_check_endpoint_url: endpoint)
end
describe '#issue_spam?' do
before do
allow_next_instance_of(::Spamcheck::SpamcheckService::Stub) do |instance|
allow(instance).to receive(:check_for_spam_issue).and_return(response)
end
end
using RSpec::Parameterized::TableSyntax
where(:verdict, :expected) do
::Spamcheck::SpamVerdict::Verdict::ALLOW | Spam::SpamConstants::ALLOW
::Spamcheck::SpamVerdict::Verdict::CONDITIONAL_ALLOW | Spam::SpamConstants::CONDITIONAL_ALLOW
::Spamcheck::SpamVerdict::Verdict::DISALLOW | Spam::SpamConstants::DISALLOW
::Spamcheck::SpamVerdict::Verdict::BLOCK | Spam::SpamConstants::BLOCK_USER
end
with_them do
let(:verdict_value) { verdict }
it "returns expected spam constant" do
expect(subject).to eq([expected, ""])
end
end
end
describe "#build_issue_protobuf", :aggregate_failures do
it 'builds the expected protobuf object' do
cxt = { action: :create }
issue_pb = described_class.new.send(:build_issue_protobuf,
issue: issue, user: user,
context: cxt)
expect(issue_pb.title).to eq issue.title
expect(issue_pb.description).to eq issue.description
expect(issue_pb.user_in_project). to be false
expect(issue_pb.created_at).to eq timestamp_to_protobuf_timestamp(issue.created_at)
expect(issue_pb.updated_at).to eq timestamp_to_protobuf_timestamp(issue.updated_at)
expect(issue_pb.action).to be ::Spamcheck::Action.lookup(::Spamcheck::Action::CREATE)
expect(issue_pb.user.username).to eq user.username
end
end
describe '#build_user_proto_buf', :aggregate_failures do
it 'builds the expected protobuf object' do
user_pb = described_class.new.send(:build_user_protobuf, user)
expect(user_pb.username).to eq user.username
expect(user_pb.org).to eq user.organization
expect(user_pb.created_at).to eq timestamp_to_protobuf_timestamp(user.created_at)
expect(user_pb.emails.count).to be 1
expect(user_pb.emails.first.email).to eq user.email
expect(user_pb.emails.first.verified).to eq user.confirmed?
end
context 'when user has multiple email addresses' do
let(:secondary_email) {create(:email, :confirmed, user: user)}
before do
user.emails << secondary_email
end
it 'adds emsils to the user pb object' do
user_pb = described_class.new.send(:build_user_protobuf, user)
expect(user_pb.emails.count).to eq 2
expect(user_pb.emails.first.email).to eq user.email
expect(user_pb.emails.first.verified).to eq user.confirmed?
expect(user_pb.emails.last.email).to eq secondary_email.email
expect(user_pb.emails.last.verified).to eq secondary_email.confirmed?
end
end
end
private
def timestamp_to_protobuf_timestamp(timestamp)
Google::Protobuf::Timestamp.new(seconds: timestamp.to_time.to_i,
nanos: timestamp.to_time.nsec)
end
end

View File

@ -216,7 +216,8 @@ RSpec.describe ApplicationSetting do
setting.spam_check_endpoint_enabled = true setting.spam_check_endpoint_enabled = true
end end
it { is_expected.to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) } it { is_expected.to allow_value('grpc://example.org/spam_check').for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value('nonsense').for(:spam_check_endpoint_url) } it { is_expected.not_to allow_value('nonsense').for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value(nil).for(:spam_check_endpoint_url) } it { is_expected.not_to allow_value(nil).for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value('').for(:spam_check_endpoint_url) } it { is_expected.not_to allow_value('').for(:spam_check_endpoint_url) }
@ -227,7 +228,8 @@ RSpec.describe ApplicationSetting do
setting.spam_check_endpoint_enabled = false setting.spam_check_endpoint_enabled = false
end end
it { is_expected.to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) } it { is_expected.to allow_value('grpc://example.org/spam_check').for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value('https://example.org/spam_check').for(:spam_check_endpoint_url) }
it { is_expected.not_to allow_value('nonsense').for(:spam_check_endpoint_url) } it { is_expected.not_to allow_value('nonsense').for(:spam_check_endpoint_url) }
it { is_expected.to allow_value(nil).for(:spam_check_endpoint_url) } it { is_expected.to allow_value(nil).for(:spam_check_endpoint_url) }
it { is_expected.to allow_value('').for(:spam_check_endpoint_url) } it { is_expected.to allow_value('').for(:spam_check_endpoint_url) }

View File

@ -123,7 +123,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
issues_create_limit: 300, issues_create_limit: 300,
raw_blob_request_limit: 300, raw_blob_request_limit: 300,
spam_check_endpoint_enabled: true, spam_check_endpoint_enabled: true,
spam_check_endpoint_url: 'https://example.com/spam_check', spam_check_endpoint_url: 'grpc://example.com/spam_check',
spam_check_api_key: 'SPAM_CHECK_API_KEY', spam_check_api_key: 'SPAM_CHECK_API_KEY',
disabled_oauth_sign_in_sources: 'unknown', disabled_oauth_sign_in_sources: 'unknown',
import_sources: 'github,bitbucket', import_sources: 'github,bitbucket',
@ -169,7 +169,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['issues_create_limit']).to eq(300) expect(json_response['issues_create_limit']).to eq(300)
expect(json_response['raw_blob_request_limit']).to eq(300) expect(json_response['raw_blob_request_limit']).to eq(300)
expect(json_response['spam_check_endpoint_enabled']).to be_truthy expect(json_response['spam_check_endpoint_enabled']).to be_truthy
expect(json_response['spam_check_endpoint_url']).to eq('https://example.com/spam_check') expect(json_response['spam_check_endpoint_url']).to eq('grpc://example.com/spam_check')
expect(json_response['spam_check_api_key']).to eq('SPAM_CHECK_API_KEY') expect(json_response['spam_check_api_key']).to eq('SPAM_CHECK_API_KEY')
expect(json_response['disabled_oauth_sign_in_sources']).to eq([]) expect(json_response['disabled_oauth_sign_in_sources']).to eq([])
expect(json_response['import_sources']).to match_array(%w(github bitbucket)) expect(json_response['import_sources']).to match_array(%w(github bitbucket))

View File

@ -9,11 +9,11 @@ RSpec.describe Spam::SpamActionService do
let(:issue) { create(:issue, project: project, author: user) } let(:issue) { create(:issue, project: project, author: user) }
let(:fake_ip) { '1.2.3.4' } let(:fake_ip) { '1.2.3.4' }
let(:fake_user_agent) { 'fake-user-agent' } let(:fake_user_agent) { 'fake-user-agent' }
let(:fake_referrer) { 'fake-http-referrer' } let(:fake_referer) { 'fake-http-referer' }
let(:env) do let(:env) do
{ 'action_dispatch.remote_ip' => fake_ip, { 'action_dispatch.remote_ip' => fake_ip,
'HTTP_USER_AGENT' => fake_user_agent, 'HTTP_USER_AGENT' => fake_user_agent,
'HTTP_REFERRER' => fake_referrer } 'HTTP_REFERER' => fake_referer }
end end
let_it_be(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
@ -80,7 +80,7 @@ RSpec.describe Spam::SpamActionService do
{ {
ip_address: fake_ip, ip_address: fake_ip,
user_agent: fake_user_agent, user_agent: fake_user_agent,
referrer: fake_referrer referer: fake_referer
} }
end end
@ -222,6 +222,38 @@ RSpec.describe Spam::SpamActionService do
end end
end end
context 'spam verdict service advises to block the user' do
before do
allow(fake_verdict_service).to receive(:execute).and_return(BLOCK_USER)
end
context 'when allow_possible_spam feature flag is false' do
before do
stub_feature_flags(allow_possible_spam: false)
end
it_behaves_like 'only checks for spam if a request is provided'
it 'marks as spam' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
expect(issue).to be_spam
end
end
context 'when allow_possible_spam feature flag is true' do
it_behaves_like 'only checks for spam if a request is provided'
it 'does not mark as spam' do
response = subject
expect(response.message).to match(expected_service_check_response_message)
expect(issue).not_to be_spam
end
end
end
context 'when spam verdict service conditionally allows' do context 'when spam verdict service conditionally allows' do
before do before do
allow(fake_verdict_service).to receive(:execute).and_return(CONDITIONAL_ALLOW) allow(fake_verdict_service).to receive(:execute).and_return(CONDITIONAL_ALLOW)

View File

@ -7,18 +7,18 @@ RSpec.describe Spam::SpamVerdictService do
let(:fake_ip) { '1.2.3.4' } let(:fake_ip) { '1.2.3.4' }
let(:fake_user_agent) { 'fake-user-agent' } let(:fake_user_agent) { 'fake-user-agent' }
let(:fake_referrer) { 'fake-http-referrer' } let(:fake_referer) { 'fake-http-referer' }
let(:env) do let(:env) do
{ 'action_dispatch.remote_ip' => fake_ip, { 'action_dispatch.remote_ip' => fake_ip,
'HTTP_USER_AGENT' => fake_user_agent, 'HTTP_USER_AGENT' => fake_user_agent,
'HTTP_REFERRER' => fake_referrer } 'HTTP_REFERER' => fake_referer }
end end
let(:request) { double(:request, env: env) } let(:request) { double(:request, env: env) }
let(:check_for_spam) { true } let(:check_for_spam) { true }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:issue) { build(:issue, author: user) } let_it_be(:issue) { create(:issue, author: user) }
let(:service) do let(:service) do
described_class.new(user: user, target: issue, request: request, options: {}) described_class.new(user: user, target: issue, request: request, options: {})
end end
@ -28,7 +28,7 @@ RSpec.describe Spam::SpamVerdictService do
before do before do
allow(service).to receive(:akismet_verdict).and_return(nil) allow(service).to receive(:akismet_verdict).and_return(nil)
allow(service).to receive(:external_verdict).and_return(nil) allow(service).to receive(:spamcheck_verdict).and_return(nil)
end end
context 'if all services return nil' do context 'if all services return nil' do
@ -63,7 +63,7 @@ RSpec.describe Spam::SpamVerdictService do
context 'and they are supported' do context 'and they are supported' do
before do before do
allow(service).to receive(:akismet_verdict).and_return(DISALLOW) allow(service).to receive(:akismet_verdict).and_return(DISALLOW)
allow(service).to receive(:external_verdict).and_return(BLOCK_USER) allow(service).to receive(:spamcheck_verdict).and_return(BLOCK_USER)
end end
it 'renders the more restrictive verdict' do it 'renders the more restrictive verdict' do
@ -74,7 +74,7 @@ RSpec.describe Spam::SpamVerdictService do
context 'and one is supported' do context 'and one is supported' do
before do before do
allow(service).to receive(:akismet_verdict).and_return('nonsense') allow(service).to receive(:akismet_verdict).and_return('nonsense')
allow(service).to receive(:external_verdict).and_return(BLOCK_USER) allow(service).to receive(:spamcheck_verdict).and_return(BLOCK_USER)
end end
it 'renders the more restrictive verdict' do it 'renders the more restrictive verdict' do
@ -85,7 +85,7 @@ RSpec.describe Spam::SpamVerdictService do
context 'and none are supported' do context 'and none are supported' do
before do before do
allow(service).to receive(:akismet_verdict).and_return('nonsense') allow(service).to receive(:akismet_verdict).and_return('nonsense')
allow(service).to receive(:external_verdict).and_return('rubbish') allow(service).to receive(:spamcheck_verdict).and_return('rubbish')
end end
it 'renders the more restrictive verdict' do it 'renders the more restrictive verdict' do
@ -150,48 +150,87 @@ RSpec.describe Spam::SpamVerdictService do
end end
end end
describe '#external_verdict' do describe '#spamcheck_verdict' do
subject { service.send(:external_verdict) } subject { service.send(:spamcheck_verdict) }
context 'if a Spam Check endpoint enabled and set to a URL' do context 'if a Spam Check endpoint enabled and set to a URL' do
let(:spam_check_body) { {} } let(:spam_check_body) { {} }
let(:spam_check_http_status) { nil } let(:endpoint_url) { "grpc://www.spamcheckurl.com/spam_check" }
let(:spam_client) do
Gitlab::Spamcheck::Client.new
end
before do before do
stub_application_setting(spam_check_endpoint_enabled: true) stub_application_setting(spam_check_endpoint_enabled: true)
stub_application_setting(spam_check_endpoint_url: "http://www.spamcheckurl.com/spam_check") stub_application_setting(spam_check_endpoint_url: endpoint_url)
stub_request(:post, /.*spamcheckurl.com.*/).to_return( body: spam_check_body.to_json, status: spam_check_http_status )
end end
context 'if the endpoint is accessible' do context 'if the endpoint is accessible' do
let(:spam_check_http_status) { 200 } let(:error) { '' }
let(:error) { nil }
let(:verdict) { nil } let(:verdict) { nil }
let(:spam_check_body) do
{ verdict: verdict, error: error } before do
allow(service).to receive(:spamcheck_client).and_return(spam_client)
allow(spam_client).to receive(:issue_spam?).and_return([verdict, error])
end end
context 'the result is a valid verdict' do context 'the result is a valid verdict' do
let(:verdict) { 'allow' } let(:verdict) { ALLOW }
it 'returns the verdict' do it 'returns the verdict' do
expect(subject).to eq ALLOW expect(subject).to eq ALLOW
end end
end end
context 'the verdict is an unexpected string' do context 'when recaptcha is enabled' do
let(:verdict) { 'this is fine' } before do
allow(Gitlab::Recaptcha).to receive(:enabled?).and_return(true)
end
it 'returns the string' do using RSpec::Parameterized::TableSyntax
expect(subject).to eq verdict
# rubocop: disable Lint/BinaryOperatorWithIdenticalOperands
where(:verdict_value, :expected) do
::Spam::SpamConstants::ALLOW | ::Spam::SpamConstants::ALLOW
::Spam::SpamConstants::CONDITIONAL_ALLOW | ::Spam::SpamConstants::CONDITIONAL_ALLOW
::Spam::SpamConstants::DISALLOW | ::Spam::SpamConstants::CONDITIONAL_ALLOW
::Spam::SpamConstants::BLOCK_USER | ::Spam::SpamConstants::CONDITIONAL_ALLOW
end
# rubocop: enable Lint/BinaryOperatorWithIdenticalOperands
with_them do
let(:verdict) { verdict_value }
it "returns expected spam constant" do
expect(subject).to eq(expected)
end
end end
end end
context 'the JSON is malformed' do context 'when recaptcha is disabled' do
let(:spam_check_body) { 'this is fine' } before do
allow(Gitlab::Recaptcha).to receive(:enabled?).and_return(false)
end
it 'returns allow' do [::Spam::SpamConstants::ALLOW,
expect(subject).to eq ALLOW ::Spam::SpamConstants::CONDITIONAL_ALLOW,
::Spam::SpamConstants::DISALLOW,
::Spam::SpamConstants::BLOCK_USER].each do |verdict_value|
let(:verdict) { verdict_value }
let(:expected) { verdict_value }
it "returns expected spam constant" do
expect(subject).to eq(expected)
end
end
end
context 'the verdict is an unexpected value' do
let(:verdict) { :this_is_fine }
it 'returns the string' do
expect(subject).to eq verdict
end end
end end
@ -219,11 +258,13 @@ RSpec.describe Spam::SpamVerdictService do
end end
end end
context 'the HTTP status is not 200' do context 'the requested is aborted' do
let(:spam_check_http_status) { 500 } before do
allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::Aborted)
end
it 'returns nil' do it 'returns nil' do
expect(subject).to be_nil expect(subject).to be(ALLOW)
end end
end end
@ -239,11 +280,11 @@ RSpec.describe Spam::SpamVerdictService do
context 'if the endpoint times out' do context 'if the endpoint times out' do
before do before do
stub_request(:post, /.*spamcheckurl.com.*/).to_timeout allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::DeadlineExceeded)
end end
it 'returns nil' do it 'returns nil' do
expect(subject).to be_nil expect(subject).to be(ALLOW)
end end
end end
end end