From d20e75a8d80c2828336cd22897ea6868d666f8a5 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 9 Jan 2016 19:30:34 +0000 Subject: [PATCH] Support Akismet spam checking for creation of issues via API Currently any spam detected by Akismet by non-members via API will be logged in a separate table in the admin page. Closes #5612 --- CHANGELOG | 1 + Gemfile | 3 +- Gemfile.lock | 2 + .../admin/application_settings_controller.rb | 2 + app/controllers/admin/spam_logs_controller.rb | 33 ++++++++++++ app/models/application_setting.rb | 8 ++- app/models/spam_log.rb | 13 +++++ app/models/spam_report.rb | 5 ++ app/models/user.rb | 1 + app/services/create_spam_log_service.rb | 13 +++++ .../application_settings/_form.html.haml | 23 +++++++-- app/views/admin/spam_logs/_spam_log.html.haml | 30 +++++++++++ app/views/admin/spam_logs/index.html.haml | 21 ++++++++ app/views/layouts/nav/_admin.html.haml | 7 +++ config/routes.rb | 2 + ...326_add_akismet_to_application_settings.rb | 8 +++ db/migrate/20160109054846_create_spam_logs.rb | 16 ++++++ db/schema.rb | 15 ++++++ doc/integration/README.md | 1 + doc/integration/akismet.md | 30 +++++++++++ doc/integration/img/akismet_settings.png | Bin 0 -> 55837 bytes features/admin/spam_logs.feature | 8 +++ features/steps/admin/spam_logs.rb | 18 +++++++ features/steps/shared/paths.rb | 4 ++ lib/api/issues.rb | 22 +++++++- lib/gitlab/akismet_helper.rb | 39 +++++++++++++++ lib/gitlab/current_settings.rb | 3 +- .../admin/spam_logs_controller_spec.rb | 47 ++++++++++++++++++ spec/factories/spam_logs.rb | 7 +++ spec/lib/gitlab/akismet_helper_spec.rb | 35 +++++++++++++ spec/requests/api/issues_spec.rb | 22 ++++++++ 31 files changed, 432 insertions(+), 7 deletions(-) create mode 100644 app/controllers/admin/spam_logs_controller.rb create mode 100644 app/models/spam_log.rb create mode 100644 app/models/spam_report.rb create mode 100644 app/services/create_spam_log_service.rb create mode 100644 app/views/admin/spam_logs/_spam_log.html.haml create mode 100644 app/views/admin/spam_logs/index.html.haml create mode 100644 db/migrate/20151231152326_add_akismet_to_application_settings.rb create mode 100644 db/migrate/20160109054846_create_spam_logs.rb create mode 100644 doc/integration/akismet.md create mode 100644 doc/integration/img/akismet_settings.png create mode 100644 features/admin/spam_logs.feature create mode 100644 features/steps/admin/spam_logs.rb create mode 100644 lib/gitlab/akismet_helper.rb create mode 100644 spec/controllers/admin/spam_logs_controller_spec.rb create mode 100644 spec/factories/spam_logs.rb create mode 100644 spec/lib/gitlab/akismet_helper_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 7a70516173..1be1f7374d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -74,6 +74,7 @@ v 8.4.0 - Don't notify users twice if they are both project watchers and subscribers (Stan Hu) - Remove gray background from layout in UI - Fix signup for OAuth providers that don't provide a name + - Support Akismet spam checking for creation of issues via API (Stan Hu) - Implement new UI for group page - Implement search inside emoji picker - Let the CI runner know about builds that this build depends on diff --git a/Gemfile b/Gemfile index c9d428a179..9f012e1a7e 100644 --- a/Gemfile +++ b/Gemfile @@ -36,8 +36,9 @@ gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' gem 'rack-oauth2', '~> 1.2.1' -# reCAPTCHA protection +# Spam and anti-bot protection gem 'recaptcha', require: 'recaptcha/rails' +gem 'akismet', '~> 2.0' # Two-factor authentication gem 'devise-two-factor', '~> 2.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index b4f7587c41..9570d387f6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -49,6 +49,7 @@ GEM addressable (2.3.8) after_commit_queue (1.3.0) activerecord (>= 3.0) + akismet (2.0.0) allocations (1.0.3) annotate (2.6.10) activerecord (>= 3.2, <= 4.3) @@ -884,6 +885,7 @@ DEPENDENCIES acts-as-taggable-on (~> 3.4) addressable (~> 2.3.8) after_commit_queue + akismet (~> 2.0) allocations (~> 1.0) annotate (~> 2.6.0) asana (~> 0.4.0) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 9943745208..1515086b16 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -79,6 +79,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :recaptcha_private_key, :sentry_enabled, :sentry_dsn, + :akismet_enabled, + :akismet_api_key, restricted_visibility_levels: [], import_sources: [] ) diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb new file mode 100644 index 0000000000..69f94dd3ba --- /dev/null +++ b/app/controllers/admin/spam_logs_controller.rb @@ -0,0 +1,33 @@ +class Admin::SpamLogsController < Admin::ApplicationController + before_action :set_spam_log, only: [:destroy] + + def index + @spam_logs = SpamLog.order(created_at: :desc).page(params[:page]) + end + + def destroy + @spam_log.destroy + message = 'Spam log was successfully destroyed.' + + if params[:remove_user] + username = @spam_log.user.username + @spam_log.user.destroy + message = "User #{username} was successfully destroyed." + end + + respond_to do |format| + format.json { render json: '{}' } + format.html { redirect_to admin_spam_logs_path, notice: message } + end + end + + private + + def set_spam_log + @spam_log = SpamLog.find(params[:id]) + end + + def spam_log_params + params[:spam_log] + end +end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 59563b8823..9cafc78f76 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -88,6 +88,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :sentry_enabled + validates :akismet_api_key, + presence: true, + if: :akismet_enabled + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| @@ -143,7 +147,9 @@ class ApplicationSetting < ActiveRecord::Base shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, - two_factor_grace_period: 48 + two_factor_grace_period: 48, + recaptcha_enabled: false, + akismet_enabled: false ) end diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb new file mode 100644 index 0000000000..132c5713e0 --- /dev/null +++ b/app/models/spam_log.rb @@ -0,0 +1,13 @@ +class SpamLog < ActiveRecord::Base + belongs_to :user + + validates :user, presence: true + + def truncated_description + if description.present? && description.length > 100 + return description[0..100] + "..." + end + + description + end +end diff --git a/app/models/spam_report.rb b/app/models/spam_report.rb new file mode 100644 index 0000000000..cdc7321b08 --- /dev/null +++ b/app/models/spam_report.rb @@ -0,0 +1,5 @@ +class SpamReport < ActiveRecord::Base + belongs_to :user + + validates :user, presence: true +end diff --git a/app/models/user.rb b/app/models/user.rb index 4214f01f6a..63c895683f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -138,6 +138,7 @@ class User < ActiveRecord::Base has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest" has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy has_one :abuse_report, dependent: :destroy + has_many :spam_log, dependent: :destroy has_many :builds, dependent: :nullify, class_name: 'Ci::Build' diff --git a/app/services/create_spam_log_service.rb b/app/services/create_spam_log_service.rb new file mode 100644 index 0000000000..59a66fde47 --- /dev/null +++ b/app/services/create_spam_log_service.rb @@ -0,0 +1,13 @@ +class CreateSpamLogService < BaseService + def initialize(project, user, params) + super(project, user, params) + end + + def execute + spam_params = params.merge({ user_id: @current_user.id, + project_id: @project.id } ) + spam_log = SpamLog.new(spam_params) + spam_log.save + spam_log + end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index b0f1a34cbe..b4e3d96d40 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -218,20 +218,37 @@ = f.label :recaptcha_enabled do = f.check_box :recaptcha_enabled Enable reCAPTCHA - %span.help-block#recaptcha_help_block Helps preventing bots from creating accounts + %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts .form-group = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2' .col-sm-10 = f.text_field :recaptcha_site_key, class: 'form-control' .help-block - Generate site and private keys here: - %a{ href: 'http://www.google.com/recaptcha', target: '_blank'} http://www.google.com/recaptcha + Generate site and private keys at + %a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha + .form-group = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2' .col-sm-10 = f.text_field :recaptcha_private_key, class: 'form-control' + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :akismet_enabled do + = f.check_box :akismet_enabled + Enable Akismet + %span.help-block#akismet_help_block Helps prevent bots from creating issues + + .form-group + = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :akismet_api_key, class: 'form-control' + .help-block + Generate API key at + %a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com + %fieldset %legend Error Reporting and Logging %p diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml new file mode 100644 index 0000000000..1c793bbf73 --- /dev/null +++ b/app/views/admin/spam_logs/_spam_log.html.haml @@ -0,0 +1,30 @@ +- user = spam_log.user +%tr + %td + = spam_log.created_at.to_s(:short) + %td + - if user + = link_to user.name, user + - else + (removed) + %td + = spam_log.source_ip + %td + = spam_log.via_api ? 'Y' : 'N' + %td + = spam_log.noteable_type + %td + = spam_log.title + %td + = spam_log.truncated_description + %td + - if user + = link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true), + data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove" + %td + - if user && !user.blocked? + = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs" + - else + .btn.btn-xs.disabled + Already Blocked + = link_to 'Remove log', [:admin, spam_log, format: :json], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr" diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml new file mode 100644 index 0000000000..439468e572 --- /dev/null +++ b/app/views/admin/spam_logs/index.html.haml @@ -0,0 +1,21 @@ +- page_title "Spam Logs" +%h3.page-title Spam Logs +%hr +- if @spam_logs.present? + .table-holder + %table.table + %thead + %tr + %th Date + %th User + %th Source IP + %th Via API? + %th Type + %th Title + %th Description + %th Primary Action + %th + = render @spam_logs + = paginate @spam_logs +- else + %h4 There are no Spam Logs diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index cffdb52cc2..8dc2058742 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -82,6 +82,13 @@ Abuse Reports %span.count= number_with_delimiter(AbuseReport.count(:all)) + = nav_link(controller: :spam_logs) do + = link_to admin_spam_logs_path, title: "Spam Logs" do + = icon('exclamation-triangle fw') + %span + Spam Logs + %span.count= number_with_delimiter(SpamLog.count(:all)) + = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do = link_to admin_application_settings_path, title: 'Settings' do = icon('cogs fw') diff --git a/config/routes.rb b/config/routes.rb index 54cc338b60..034bfaf1bc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -211,6 +211,8 @@ Rails.application.routes.draw do end resources :abuse_reports, only: [:index, :destroy] + resources :spam_logs, only: [:index, :destroy] + resources :applications resources :groups, constraints: { id: /[^\/]+/ } do diff --git a/db/migrate/20151231152326_add_akismet_to_application_settings.rb b/db/migrate/20151231152326_add_akismet_to_application_settings.rb new file mode 100644 index 0000000000..3f52c758f9 --- /dev/null +++ b/db/migrate/20151231152326_add_akismet_to_application_settings.rb @@ -0,0 +1,8 @@ +class AddAkismetToApplicationSettings < ActiveRecord::Migration + def change + change_table :application_settings do |t| + t.boolean :akismet_enabled, default: false + t.string :akismet_api_key + end + end +end diff --git a/db/migrate/20160109054846_create_spam_logs.rb b/db/migrate/20160109054846_create_spam_logs.rb new file mode 100644 index 0000000000..f12fe9f8f7 --- /dev/null +++ b/db/migrate/20160109054846_create_spam_logs.rb @@ -0,0 +1,16 @@ +class CreateSpamLogs < ActiveRecord::Migration + def change + create_table :spam_logs do |t| + t.integer :user_id + t.string :source_ip + t.string :user_agent + t.boolean :via_api + t.integer :project_id + t.string :noteable_type + t.string :title + t.text :description + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 2ad2c23fba..d546e06cd8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -64,6 +64,8 @@ ActiveRecord::Schema.define(version: 20160128233227) do t.integer "metrics_sample_interval", default: 15 t.boolean "sentry_enabled", default: false t.string "sentry_dsn" + t.boolean "akismet_enabled", default: false + t.string "akismet_api_key" end create_table "audit_events", force: :cascade do |t| @@ -770,6 +772,19 @@ ActiveRecord::Schema.define(version: 20160128233227) do add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree + create_table "spam_logs", force: :cascade do |t| + t.integer "user_id" + t.string "source_ip" + t.string "user_agent" + t.boolean "via_api" + t.integer "project_id" + t.string "noteable_type" + t.string "title" + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "subscriptions", force: :cascade do |t| t.integer "user_id" t.integer "subscribable_id" diff --git a/doc/integration/README.md b/doc/integration/README.md index 83116bc837..281eea8363 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -15,6 +15,7 @@ See the documentation below for details on how to configure these services. - [OAuth2 provider](oauth_provider.md) OAuth2 application creation - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages - [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users +- [Akismet](akismet.md) Configure Akismet to stop spam GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md new file mode 100644 index 0000000000..5cc09bd536 --- /dev/null +++ b/doc/integration/akismet.md @@ -0,0 +1,30 @@ +# Akismet + +GitLab leverages [Akismet](http://akismet.com) to protect against spam. Currently +GitLab uses Akismet to prevent users who are not members of a project from +creating spam via the GitLab API. Detected spam will be rejected, and +an entry in the "Spam Log" section in the Admin page will be created. + +Privacy note: GitLab submits the user's IP and user agent to Akismet. Note that +adding a user to a project will disable the Akismet check and prevent this +from happening. + +## Configuration + +To use Akismet: + +1. Go to the URL: https://akismet.com/account/ + +2. Sign-in or create a new account. + +3. Click on "Show" to reveal the API key. + +4. Go to Applications Settings on Admin Area (`admin/application_settings`) + +5. Check the `Enable Akismet` checkbox + +6. Fill in the API key from step 3. + +7. Save the configuration. + +![Screenshot of Akismet settings](img/akismet_settings.png) diff --git a/doc/integration/img/akismet_settings.png b/doc/integration/img/akismet_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..ccdd3adb1c57acee6fbd9efc4bd8556815a75676 GIT binary patch literal 55837 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 Bwp#!I literal 0 HcmV?d00001 diff --git a/features/admin/spam_logs.feature b/features/admin/spam_logs.feature new file mode 100644 index 0000000000..92a5389e3a --- /dev/null +++ b/features/admin/spam_logs.feature @@ -0,0 +1,8 @@ +Feature: Admin spam logs + Background: + Given I sign in as an admin + And spam logs exist + + Scenario: Browse spam logs + When I visit spam logs page + Then I should see list of spam logs diff --git a/features/steps/admin/spam_logs.rb b/features/steps/admin/spam_logs.rb new file mode 100644 index 0000000000..79d18b036b --- /dev/null +++ b/features/steps/admin/spam_logs.rb @@ -0,0 +1,18 @@ +class Spinach::Features::AdminSpamLogs < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedAdmin + + step 'I should see list of spam logs' do + page.should have_content("Spam Logs") + spam_log = SpamLog.first + page.should have_content spam_log.title + page.should have_content spam_log.description + page.should have_link("Remove user") + page.should have_link("Block user") + end + + step 'spam logs exist' do + create(:spam_log) + end +end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 4264c9c6f1..2c854ac7bf 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -191,6 +191,10 @@ module SharedPaths visit admin_application_settings_path end + step 'I visit spam logs page' do + visit admin_spam_logs_path + end + step 'I visit applications page' do visit admin_applications_path end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 6e7a767207..cdadd13c13 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -3,6 +3,8 @@ module API class Issues < Grape::API before { authenticate! } + helpers ::Gitlab::AkismetHelper + helpers do def filter_issues_state(issues, state) case state @@ -19,6 +21,15 @@ module API def filter_issues_milestone(issues, milestone) issues.includes(:milestone).where('milestones.title' => milestone) end + + def create_spam_log(project, current_user, attrs) + params = attrs.dup + params[:source_ip] = env['REMOTE_ADDR'] + params[:user_agent] = env['HTTP_USER_AGENT'] + params[:noteable_type] = 'Issue' + params[:via_api] = true + ::CreateSpamLogService.new(project, current_user, params).execute + end end resource :issues do @@ -114,7 +125,16 @@ module API render_api_error!({ labels: errors }, 400) end - issue = ::Issues::CreateService.new(user_project, current_user, attrs).execute + project = user_project + text = attrs[:title] + text += "\n#{attrs[:description]}" if attrs[:description].present? + + if check_for_spam?(project, current_user) && is_spam?(env, current_user, text) + create_spam_log(project, current_user, attrs) + render_api_error!({ error: 'Spam detected' }, 400) + end + + issue = ::Issues::CreateService.new(project, current_user, attrs).execute if issue.valid? # Find or create labels and attach to issue. Labels are valid because diff --git a/lib/gitlab/akismet_helper.rb b/lib/gitlab/akismet_helper.rb new file mode 100644 index 0000000000..71f525309f --- /dev/null +++ b/lib/gitlab/akismet_helper.rb @@ -0,0 +1,39 @@ +module Gitlab + module AkismetHelper + def akismet_enabled? + current_application_settings.akismet_enabled + end + + def akismet_client + ::Akismet::Client.new(current_application_settings.akismet_api_key, + Gitlab.config.gitlab.url) + end + + def check_for_spam?(project, user) + akismet_enabled? && !project.team.member?(user) + end + + def is_spam?(environment, user, text) + client = akismet_client + ip_address = environment['REMOTE_ADDR'] + user_agent = environment['HTTP_USER_AGENT'] + + params = { + type: 'comment', + text: text, + created_at: DateTime.now, + author: user.name, + author_email: user.email, + referrer: environment['HTTP_REFERER'], + } + + begin + is_spam, is_blatant = client.check(ip_address, user_agent, params) + is_spam || is_blatant + rescue => e + Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check") + false + end + end + end +end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index a6b2f14521..8531c7e87e 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -34,7 +34,8 @@ module Gitlab shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], max_artifacts_size: Settings.artifacts['max_size'], require_two_factor_authentication: false, - two_factor_grace_period: 48 + two_factor_grace_period: 48, + akismet_enabled: false ) end diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb new file mode 100644 index 0000000000..2486298fc7 --- /dev/null +++ b/spec/controllers/admin/spam_logs_controller_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Admin::SpamLogsController do + let(:admin) { create(:admin) } + let(:spam_log) { create(:spam_log, user: admin) } + + before do + sign_in(admin) + end + + describe '#index' do + it 'lists all spam logs' do + get :index + expect(response.status).to eq(200) + end + end + + describe '#destroy' do + it 'destroys just spam log' do + user = spam_log.user + delete :destroy, id: spam_log.id + + expect(SpamLog.all.count).to eq(0) + expect(User.find(user.id)).to be_truthy + expect(response.status).to eq(302) + end + + it 'destroys user and his spam logs' do + user = spam_log.user + delete :destroy, id: spam_log.id, remove_user: true + + expect(SpamLog.all.count).to eq(0) + expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) + expect(response.status).to eq(302) + end + + it 'destroys user and his spam logs with JSON format' do + user = spam_log.user + delete :destroy, id: spam_log.id, remove_user: true, format: :json + + expect(SpamLog.all.count).to eq(0) + expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) + expect(JSON.parse(response.body)).to eq({}) + expect(response.status).to eq(200) + end + end +end diff --git a/spec/factories/spam_logs.rb b/spec/factories/spam_logs.rb new file mode 100644 index 0000000000..9e8686d73c --- /dev/null +++ b/spec/factories/spam_logs.rb @@ -0,0 +1,7 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :spam_log do + user + end +end diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb new file mode 100644 index 0000000000..9858935180 --- /dev/null +++ b/spec/lib/gitlab/akismet_helper_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Gitlab::AkismetHelper, type: :helper do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url)) + current_application_settings.akismet_enabled = true + current_application_settings.akismet_api_key = '12345' + end + + describe '#check_for_spam?' do + it 'returns true for non-member' do + expect(helper.check_for_spam?(project, user)).to eq(true) + end + + it 'returns false for member' do + project.team << [user, :guest] + expect(helper.check_for_spam?(project, user)).to eq(false) + end + end + + describe '#is_spam?' do + it 'returns true for spam' do + environment = { + 'REMOTE_ADDR' => '127.0.0.1', + 'HTTP_USER_AGENT' => 'Test User Agent' + } + + allow_any_instance_of(::Akismet::Client).to receive(:check).and_return([true, true]) + expect(helper.is_spam?(environment, user, 'Is this spam?')).to eq(true) + end + end +end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 5e65ad18c0..2e50344c14 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -241,6 +241,28 @@ describe API::API, api: true do end end + describe 'POST /projects/:id/issues with spam filtering' do + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:check_for_spam?).and_return(true) + allow(endpoint).to receive(:is_spam?).and_return(true) + end + end + + it "should create a new project issue" do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', labels: 'label, label2' + expect(response.status).to eq(400) + expect(json_response['message']).to eq({ "error" => "Spam detected" }) + spam_logs = SpamLog.all + expect(spam_logs.count).to eq(1) + expect(spam_logs[0].title).to eq('new issue') + expect(spam_logs[0].user).to eq(user) + expect(spam_logs[0].noteable_type).to eq('Issue') + expect(spam_logs[0].project_id).to eq(project.id) + end + end + describe "PUT /projects/:id/issues/:issue_id to update only title" do it "should update a project issue" do put api("/projects/#{project.id}/issues/#{issue.id}", user),