diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb index b27185a6addd29b5503ef50ddb023c38bbac7cd7..d7ed6aa33ef209b4fafee2220dbd4b01b63e96f4 100644 --- a/app/controllers/admin/spam_logs_controller.rb +++ b/app/controllers/admin/spam_logs_controller.rb @@ -5,7 +5,9 @@ class Admin::SpamLogsController < Admin::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def index - @spam_logs = SpamLog.includes(:user).order(id: :desc).page(params[:page]).without_count + @spam_logs = SpamLog.preload(user: [:trusted_with_spam_attribute]) + .order(id: :desc) + .page(params[:page]).without_count end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 1f05e4e7b21c267ae5712ee4d86f0c8f8179d7e8..9bb0d902119f26129ae2c4011498441d02db14ac 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -164,6 +164,26 @@ def unlock end end + def trust + result = Users::TrustService.new(current_user).execute(user) + + if result[:status] == :success + redirect_back_or_admin_user(notice: _("Successfully trusted")) + else + redirect_back_or_admin_user(alert: _("Error occurred. User was not updated")) + end + end + + def untrust + result = Users::UntrustService.new(current_user).execute(user) + + if result[:status] == :success + redirect_back_or_admin_user(notice: _("Successfully untrusted")) + else + redirect_back_or_admin_user(alert: _("Error occurred. User was not updated")) + end + end + def confirm if update_user(&:force_confirm) redirect_back_or_admin_user(notice: _("Successfully confirmed")) diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml index 6aed8508a6a48fb50bb4277c39a5f19108876832..878692438d47c4ec011728058480c0360948f80e 100644 --- a/app/views/admin/spam_logs/_spam_log.html.haml +++ b/app/views/admin/spam_logs/_spam_log.html.haml @@ -29,7 +29,7 @@ variant: :danger, method: :delete, href: admin_spam_log_path(spam_log, remove_user: true), - button_options: { data: { confirm: _("USER %{user_name} WILL BE REMOVED! Are you sure?") % { user_name: user.name }, confirm_btn_variant: 'danger' }, aria: { label: _('Remove user') } }) do + button_options: { data: { confirm: _("User %{user_name} will be removed! Are you sure?") % { user_name: user.name }, confirm_btn_variant: 'danger' }, aria: { label: _('Remove user') } }) do = _('Remove user') %td -# TODO: Remove conditonal once spamcheck supports this https://gitlab.com/gitlab-com/gl-security/engineering-and-research/automation-team/spam/spamcheck/-/issues/190 @@ -48,11 +48,23 @@ = render Pajamas::ButtonComponent.new(size: :small, method: :put, href: block_admin_user_path(user), - button_options: { class: 'gl-mb-3', data: {confirm: _('USER WILL BE BLOCKED! Are you sure?')} }) do + button_options: { class: 'gl-mb-3', data: {confirm: _('User will be blocked! Are you sure?')} }) do = _('Block user') - else = render Pajamas::ButtonComponent.new(size: :small, button_options: { class: 'disabled gl-mb-3'}) do = _("Already blocked") + - if user && !user.trusted? + = render Pajamas::ButtonComponent.new(size: :small, + method: :put, + href: trust_admin_user_path(user), + button_options: { class: 'gl-mb-3', data: {confirm: _('User will be allowed to create possible spam! Are you sure?')} }) do + = _('Trust user') + - else + = render Pajamas::ButtonComponent.new(size: :small, + method: :put, + href: untrust_admin_user_path(user), + button_options: { class: 'gl-mb-3', data: {confirm: _('User will not be allowed to create possible spam! Are you sure?')} }) do + = _('Untrust user') = render Pajamas::ButtonComponent.new(size: :small, method: :delete, href: [:admin, spam_log], diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 5513ac1813a0a593db9a6606165c9ad94c515356..e8ad19624e9823332df99c882050e74189a3dc9c 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -22,6 +22,8 @@ put :unlock put :confirm put :approve + put :trust + put :untrust delete :reject post :impersonate patch :disable_two_factor diff --git a/doc/administration/review_spam_logs.md b/doc/administration/review_spam_logs.md new file mode 100644 index 0000000000000000000000000000000000000000..35cc78a9bf36d3402d6fc4524b5d287db1194ae5 --- /dev/null +++ b/doc/administration/review_spam_logs.md @@ -0,0 +1,52 @@ +--- +stage: Govern +group: Anti-Abuse +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +type: reference, howto +--- + +# Review spam logs **(FREE SELF)** + +GitLab tracks user activity and flags certain behavior for potential spam. + +In the Admin Area, a GitLab administrator can view and resolve spam logs. + +## Manage spam logs + +> **Trust user** [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131812) in GitLab 16.5. + +View and resolve spam logs to moderate user activity in your instance. + +To view spam logs: + +1. On the left sidebar, select **Search or go to**. +1. Select **Admin Area**. +1. Select **Spam Logs**. +1. Optional. To resolve a spam log, select a log and then select **Remove user**, **Block user**, **Remove log**, or **Trust user**. + +### Resolving spam logs + +You can resolve a spam log with one of the following effects: + +| Option | Description | +|---------|-------------| +| **Remove user** | The user is [deleted](../user/profile/account/delete_account.md) from the instance. | +| **Block user** | The user is blocked from the instance. The spam log remains in the list. | +| **Remove log** | The spam log is removed from the list. | +| **Trust user** | The user is trusted, and can create issues, notes, snippets, and merge requests without being blocked for spam. Spam logs are not created for trusted users. | + +NOTE: +Users can be [blocked](../api/users.md#block-user) and +[unblocked](../api/users.md#unblock-user) using the GitLab API. + + diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1d3174577aaeb070f9cd946a30445e6ae411475f..d37a5783774481cfe5a22c3d69b5239f46ff1a33 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19033,6 +19033,9 @@ msgstr "" msgid "Error occurred. User was not unlocked" msgstr "" +msgid "Error occurred. User was not updated" +msgstr "" + msgid "Error parsing CSV file. Please make sure it has" msgstr "" @@ -46226,6 +46229,9 @@ msgstr "" msgid "Successfully synced %{synced_timeago}." msgstr "" +msgid "Successfully trusted" +msgstr "" + msgid "Successfully unbanned" msgstr "" @@ -46238,6 +46244,9 @@ msgstr "" msgid "Successfully unlocked" msgstr "" +msgid "Successfully untrusted" +msgstr "" + msgid "Successfully updated %{last_updated_timeago}." msgstr "" @@ -50229,6 +50238,9 @@ msgstr "" msgid "Trigger|Trigger description" msgstr "" +msgid "Trust user" +msgstr "" + msgid "Trusted" msgstr "" @@ -50379,12 +50391,6 @@ msgstr "" msgid "URL or request ID" msgstr "" -msgid "USER %{user_name} WILL BE REMOVED! Are you sure?" -msgstr "" - -msgid "USER WILL BE BLOCKED! Are you sure?" -msgstr "" - msgid "UTC" msgstr "" @@ -50733,6 +50739,9 @@ msgstr "" msgid "Untitled" msgstr "" +msgid "Untrust user" +msgstr "" + msgid "Unused" msgstr "" @@ -51404,6 +51413,9 @@ msgstr "" msgid "User %{current_user_username} has started impersonating %{username}" msgstr "" +msgid "User %{user_name} will be removed! Are you sure?" +msgstr "" + msgid "User %{username} was successfully removed." msgstr "" @@ -51512,6 +51524,15 @@ msgstr "" msgid "User was successfully updated." msgstr "" +msgid "User will be allowed to create possible spam! Are you sure?" +msgstr "" + +msgid "User will be blocked! Are you sure?" +msgstr "" + +msgid "User will not be allowed to create possible spam! Are you sure?" +msgstr "" + msgid "User-based escalation rules must have a user with access to the project" msgstr "" diff --git a/spec/features/admin/admin_browse_spam_logs_spec.rb b/spec/features/admin/admin_browse_spam_logs_spec.rb index c272a8630b76deb3987a4f66e13d262ccb8e4bb0..f781e2adf0748d9c2a30bf4971b2c1709baf07cd 100644 --- a/spec/features/admin/admin_browse_spam_logs_spec.rb +++ b/spec/features/admin/admin_browse_spam_logs_spec.rb @@ -4,9 +4,9 @@ RSpec.describe 'Admin browse spam logs', feature_category: :shared do let!(:spam_log) { create(:spam_log, description: 'abcde ' * 20) } + let(:admin) { create(:admin) } before do - admin = create(:admin) sign_in(admin) gitlab_enable_admin_mode_sign_in(admin) end @@ -22,6 +22,7 @@ expect(page).to have_content("#{spam_log.description[0...97]}...") expect(page).to have_link('Remove user') expect(page).to have_link('Block user') + expect(page).to have_link('Trust user') end it 'does not perform N+1 queries' do @@ -30,4 +31,15 @@ expect { visit admin_spam_logs_path }.not_to exceed_query_limit(control_queries) end + + context 'when user is trusted' do + before do + UserCustomAttribute.set_trusted_by(user: spam_log.user, trusted_by: admin) + end + + it 'allows admin to untrust the user' do + visit admin_spam_logs_path + expect(page).to have_link('Untrust user') + end + end end diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb index e525d615b50311c6e714d2d15501ad1956afc344..2f8025691f43da121c7d9a133c14424234e5c105 100644 --- a/spec/requests/admin/users_controller_spec.rb +++ b/spec/requests/admin/users_controller_spec.rb @@ -74,4 +74,54 @@ expect { request }.to change { user.reload.access_locked? }.from(true).to(false) end end + + describe 'PUT #trust' do + subject(:request) { put trust_admin_user_path(user) } + + it 'trusts the user' do + expect { request }.to change { user.reload.trusted? }.from(false).to(true) + end + + context 'when setting trust fails' do + before do + allow_next_instance_of(Users::TrustService) do |instance| + allow(instance).to receive(:execute).and_return({ status: :failed }) + end + end + + it 'displays a flash alert' do + request + + expect(response).to redirect_to(admin_user_path(user)) + expect(flash[:alert]).to eq(s_('Error occurred. User was not updated')) + end + end + end + + describe 'PUT #untrust' do + before do + user.custom_attributes.create!(key: UserCustomAttribute::TRUSTED_BY, value: "placeholder") + end + + subject(:request) { put untrust_admin_user_path(user) } + + it 'trusts the user' do + expect { request }.to change { user.reload.trusted? }.from(true).to(false) + end + + context 'when untrusting fails' do + before do + allow_next_instance_of(Users::UntrustService) do |instance| + allow(instance).to receive(:execute).and_return({ status: :failed }) + end + end + + it 'displays a flash alert' do + request + + expect(response).to redirect_to(admin_user_path(user)) + expect(flash[:alert]).to eq(s_('Error occurred. User was not updated')) + end + end + end end