diff --git a/app/assets/javascripts/admin/abuse_report/constants.js b/app/assets/javascripts/admin/abuse_report/constants.js index 6cae6b24f20a0498e8e8f69acf6ac4f4e8af6fdb..62561630b18821bd7ddc97b69d70bf3c4762c4b9 100644 --- a/app/assets/javascripts/admin/abuse_report/constants.js +++ b/app/assets/javascripts/admin/abuse_report/constants.js @@ -30,6 +30,7 @@ export const USER_ACTION_OPTIONS = [ NO_ACTION, { value: 'block_user', text: s__('AbuseReport|Block user') }, { value: 'ban_user', text: s__('AbuseReport|Ban user') }, + { value: 'trust_user', text: s__('AbuseReport|Trust user') }, { value: 'delete_user', text: s__('AbuseReport|Delete user') }, ]; @@ -48,6 +49,7 @@ export const REASON_OPTIONS = [ text: s__('AbuseReport|Confirmed violation of a copyright or a trademark'), }, { value: 'malware', text: s__('AbuseReport|Confirmed posting of malware') }, + { value: 'trusted', text: s__(`AbuseReport|Confirmed trusted user`) }, { value: 'other', text: s__('AbuseReport|Something else') }, { value: 'unconfirmed', text: s__('AbuseReport|Abuse unconfirmed') }, ]; diff --git a/app/assets/javascripts/admin/users/components/actions/index.js b/app/assets/javascripts/admin/users/components/actions/index.js index 4e63a85df891f247ad9353718fb40c4682bf3737..633bc4d8b15c32793d8a64bad40e6d038d7380d2 100644 --- a/app/assets/javascripts/admin/users/components/actions/index.js +++ b/app/assets/javascripts/admin/users/components/actions/index.js @@ -9,6 +9,8 @@ import Reject from './reject.vue'; import Unban from './unban.vue'; import Unblock from './unblock.vue'; import Unlock from './unlock.vue'; +import Trust from './trust_user.vue'; +import Untrust from './untrust_user.vue'; export default { Activate, @@ -22,4 +24,6 @@ export default { Unblock, Unlock, Reject, + Trust, + Untrust, }; diff --git a/app/assets/javascripts/admin/users/components/actions/trust_user.vue b/app/assets/javascripts/admin/users/components/actions/trust_user.vue new file mode 100644 index 0000000000000000000000000000000000000000..2722442ae0d258407bf9c19d7cc828971fc67008 --- /dev/null +++ b/app/assets/javascripts/admin/users/components/actions/trust_user.vue @@ -0,0 +1,61 @@ + + + diff --git a/app/assets/javascripts/admin/users/components/actions/untrust_user.vue b/app/assets/javascripts/admin/users/components/actions/untrust_user.vue new file mode 100644 index 0000000000000000000000000000000000000000..919e9dfc4f20667975d403e97c9019f19bd0e451 --- /dev/null +++ b/app/assets/javascripts/admin/users/components/actions/untrust_user.vue @@ -0,0 +1,55 @@ + + + diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js index 9cd61d6b1dbcc676fa2ae8201224c0a3c3bc11da..43c9a8749cde76f38d4d2b5f47c8631534322cad 100644 --- a/app/assets/javascripts/admin/users/constants.js +++ b/app/assets/javascripts/admin/users/constants.js @@ -19,4 +19,6 @@ export const I18N_USER_ACTIONS = { deleteWithContributions: s__('AdminUsers|Delete user and contributions'), ban: s__('AdminUsers|Ban user'), unban: s__('AdminUsers|Unban user'), + trust: s__('AdminUsers|Trust user'), + untrust: s__('AdminUsers|Untrust user'), }; 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..ae6311c4d09f0d9430b26ccface1f1a8494bf397 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")) @@ -290,7 +310,7 @@ def paginate_without_count? end def users_with_included_associations(users) - users.includes(:authorized_projects) # rubocop: disable CodeReuse/ActiveRecord + users.includes(:authorized_projects, :trusted_with_spam_attribute) # rubocop: disable CodeReuse/ActiveRecord end def admin_making_changes_for_another_user? diff --git a/app/helpers/admin/user_actions_helper.rb b/app/helpers/admin/user_actions_helper.rb index 969c5d5a0b5b1432deba0a9d6437b758c0dc3849..ba40b3c8a8df5a6f639ce153ee85e35797f59cd9 100644 --- a/app/helpers/admin/user_actions_helper.rb +++ b/app/helpers/admin/user_actions_helper.rb @@ -16,6 +16,7 @@ def admin_actions(user) unlock_actions delete_actions ban_actions + trust_actions @actions end @@ -66,5 +67,19 @@ def ban_actions @actions << 'ban' end end + + def trust_actions + return if @user.internal? || + @user.blocked_pending_approval? || + @user.banned? || + @user.blocked? || + @user.deactivated? + + @actions << if @user.trusted? + 'untrust' + else + 'trust' + end + end end end diff --git a/app/helpers/resource_events/abuse_report_events_helper.rb b/app/helpers/resource_events/abuse_report_events_helper.rb index 8adbc8911840020232594565ac826874a640a88c..207ec73454b2ad7a400bd6fc7933dcf16594633f 100644 --- a/app/helpers/resource_events/abuse_report_events_helper.rb +++ b/app/helpers/resource_events/abuse_report_events_helper.rb @@ -10,6 +10,8 @@ def success_message_for_action(action) s_('AbuseReportEvent|Successfully blocked the user') when 'delete_user' s_('AbuseReportEvent|Successfully scheduled the user for deletion') + when 'trust_user' + s_('AbuseReportEvent|Successfully trusted the user') when 'close_report' s_('AbuseReportEvent|Successfully closed the report') when 'ban_user_and_close_report' @@ -18,6 +20,8 @@ def success_message_for_action(action) s_('AbuseReportEvent|Successfully blocked the user and closed the report') when 'delete_user_and_close_report' s_('AbuseReportEvent|Successfully scheduled the user for deletion and closed the report') + when 'trust_user_and_close_report' + s_('AbuseReportEvent|Successfully trusted the user and closed the report') end end end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index ac279904fd27e95811f36875490df47fa0adef98..d5e9b0a095d4b03908ae8425817ccdee608b9b3d 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -260,7 +260,9 @@ def admin_users_paths delete_with_contributions: admin_user_path(:id, hard_delete: true), admin_user: admin_user_path(:id), ban: ban_admin_user_path(:id), - unban: unban_admin_user_path(:id) + unban: unban_admin_user_path(:id), + trust: trust_admin_user_path(:id), + untrust: untrust_admin_user_path(:id) } end diff --git a/app/models/resource_events/abuse_report_event.rb b/app/models/resource_events/abuse_report_event.rb index 59f88a63998eec8a55449305955a76b790028f8a..5881f87241d8a837f7869c195b054ae8e3822455 100644 --- a/app/models/resource_events/abuse_report_event.rb +++ b/app/models/resource_events/abuse_report_event.rb @@ -16,7 +16,9 @@ class AbuseReportEvent < ApplicationRecord close_report: 4, ban_user_and_close_report: 5, block_user_and_close_report: 6, - delete_user_and_close_report: 7 + delete_user_and_close_report: 7, + trust_user: 8, + trust_user_and_close_report: 9 } enum reason: { @@ -28,7 +30,8 @@ class AbuseReportEvent < ApplicationRecord copyright: 6, malware: 7, other: 8, - unconfirmed: 9 + unconfirmed: 9, + trusted: 10 } def success_message diff --git a/app/models/user.rb b/app/models/user.rb index 507a58dfd4900b689a3be78c87770bd88fa12527..5d0a065fd3dedf98211992a912034283afa565d6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -249,6 +249,7 @@ def update_tracked_fields!(request) has_many :bulk_imports has_many :custom_attributes, class_name: 'UserCustomAttribute' + has_one :trusted_with_spam_attribute, -> { UserCustomAttribute.trusted_with_spam }, class_name: 'UserCustomAttribute' has_many :callouts, class_name: 'Users::Callout' has_many :group_callouts, class_name: 'Users::GroupCallout' has_many :project_callouts, class_name: 'Users::ProjectCallout' @@ -565,6 +566,12 @@ def blocked? scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) } scope :by_ids_or_usernames, -> (ids, usernames) { where(username: usernames).or(where(id: ids)) } scope :without_forbidden_states, -> { where.not(state: FORBIDDEN_SEARCH_STATES) } + scope :trusted, -> do + where('EXISTS (?)', ::UserCustomAttribute + .select(1) + .where('user_id = users.id') + .trusted_with_spam) + end strip_attributes! :name @@ -733,6 +740,8 @@ def filter_items(filter_name) external when 'deactivated' deactivated + when "trusted" + trusted else active_without_ghosts end @@ -2193,8 +2202,8 @@ def abuse_metadata } end - def allow_possible_spam? - custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM).exists? + def trusted? + trusted_with_spam_attribute.present? end def namespace_commit_email_for_namespace(namespace) diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb index 425f2cc062b068c4f65ad04ee5d5c4c58e4f6dc2..2dee3a25994309c5ec10063e4601fcd9f77f628d 100644 --- a/app/models/user_custom_attribute.rb +++ b/app/models/user_custom_attribute.rb @@ -10,12 +10,13 @@ class UserCustomAttribute < ApplicationRecord scope :by_user_id, ->(user_id) { where(user_id: user_id) } scope :by_updated_at, ->(updated_at) { where(updated_at: updated_at) } scope :arkose_sessions, -> { by_key('arkose_session') } + scope :trusted_with_spam, -> { by_key(TRUSTED_BY) } BLOCKED_BY = 'blocked_by' UNBLOCKED_BY = 'unblocked_by' ARKOSE_RISK_BAND = 'arkose_risk_band' AUTO_BANNED_BY_ABUSE_REPORT_ID = 'auto_banned_by_abuse_report_id' - ALLOW_POSSIBLE_SPAM = 'allow_possible_spam' + TRUSTED_BY = 'trusted_by' IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt' class << self @@ -45,6 +46,18 @@ def set_banned_by_abuse_report(abuse_report) upsert_custom_attributes([custom_attribute]) end + def set_trusted_by(user:, trusted_by:) + return unless user && trusted_by + + custom_attribute = { + user_id: user.id, + key: UserCustomAttribute::TRUSTED_BY, + value: "#{trusted_by.username}/#{trusted_by.id}+#{Time.current}" + } + + upsert_custom_attributes([custom_attribute]) + end + private def blocked_users diff --git a/app/services/admin/abuse_reports/moderate_user_service.rb b/app/services/admin/abuse_reports/moderate_user_service.rb index da61a4dc8f60be161a0010a065fbf1a29e9e73dc..9a214425c41581afbf2a76489e9b18f648ffb811 100644 --- a/app/services/admin/abuse_reports/moderate_user_service.rb +++ b/app/services/admin/abuse_reports/moderate_user_service.rb @@ -42,6 +42,7 @@ def perform_action when :block_user then block_user when :delete_user then delete_user when :close_report then close_report + when :trust_user then trust_user end end @@ -65,6 +66,10 @@ def close_report success end + def trust_user + Users::TrustService.new(current_user).execute(abuse_report.user) + end + def close_report_and_record_event event = action diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb index 9efe51b43b815ee3c03b7f5351602dd37471d14f..2d4bebc8b2bb6065253ca1cbc35b26ad7fc49449 100644 --- a/app/services/spam/spam_verdict_service.rb +++ b/app/services/spam/spam_verdict_service.rb @@ -90,7 +90,7 @@ def override_via_allow_possible_spam?(verdict:) end def allow_possible_spam? - target.allow_possible_spam?(user) || user.allow_possible_spam? + target.allow_possible_spam?(user) || user.trusted? end def spamcheck_client diff --git a/app/services/users/allow_possible_spam_service.rb b/app/services/users/allow_possible_spam_service.rb deleted file mode 100644 index d9273fe0fc1cf462e7fd116870de64ff1a24507a..0000000000000000000000000000000000000000 --- a/app/services/users/allow_possible_spam_service.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Users - class AllowPossibleSpamService < BaseService - def initialize(current_user) - @current_user = current_user - end - - def execute(user) - custom_attribute = { - user_id: user.id, - key: UserCustomAttribute::ALLOW_POSSIBLE_SPAM, - value: "#{current_user.username}/#{current_user.id}+#{Time.current}" - } - UserCustomAttribute.upsert_custom_attributes([custom_attribute]) - end - end -end diff --git a/app/services/users/disallow_possible_spam_service.rb b/app/services/users/trust_service.rb similarity index 53% rename from app/services/users/disallow_possible_spam_service.rb rename to app/services/users/trust_service.rb index e31ba7ddff02a481ef0d0cd1b11beedb2e2336bd..faf0b9c40eabc0a6f4741b2fd5e2ba1048f0d929 100644 --- a/app/services/users/disallow_possible_spam_service.rb +++ b/app/services/users/trust_service.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true module Users - class DisallowPossibleSpamService < BaseService + class TrustService < BaseService def initialize(current_user) @current_user = current_user end def execute(user) - user.custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM).delete_all + UserCustomAttribute.set_trusted_by(user: user, trusted_by: @current_user) + success end end end diff --git a/app/services/users/untrust_service.rb b/app/services/users/untrust_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..6db05d6fa7765c8fa50309111108a7f82d30b35e --- /dev/null +++ b/app/services/users/untrust_service.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Users + class UntrustService < BaseService + def initialize(current_user) + @current_user = current_user + end + + def execute(user) + user.custom_attributes.by_key(UserCustomAttribute::TRUSTED_BY).delete_all + success + end + end +end diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..ea0433b789d82d12e0b92f698fe9aee8c1611d94 --- /dev/null +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -0,0 +1,45 @@ +- reporter = abuse_report.reporter +- user = abuse_report.user +%tr + %th.d-block.d-sm-none + %strong= _('User') + %td + - if user + = link_to user.name, user + .light.small + = html_escape(_('Joined %{time_ago}')) % { time_ago: time_ago_with_tooltip(user.created_at).html_safe } + - else + = _('(removed)') + %td + - if reporter + %strong.subheading.d-block.d-sm-none + = _('Reported by %{reporter}').html_safe % { reporter: reporter ? link_to(reporter.name, reporter) : _('(removed)') } + .light.gl-display-none.gl-sm-display-block + = link_to(reporter.name, reporter) + .light.small + = time_ago_with_tooltip(abuse_report.created_at) + - else + = _('(removed)') + %td + %strong.subheading.d-block.d-sm-none + = _('Message') + .message + = markdown_field(abuse_report, :message) + %td + - if user && user != current_user + = render Pajamas::ButtonComponent.new(href: admin_abuse_report_path(abuse_report, remove_user: true), variant: :danger, block: true, button_options: { data: { confirm: _("USER %{user} WILL BE REMOVED! Are you sure?") % { user: user.name }, confirm_btn_variant: "danger", remote: true, method: :delete }, class: "js-remove-tr" }) do + = _('Remove user & report') + - if user.blocked? + = render Pajamas::ButtonComponent.new(href: block_admin_user_path(user), block: true, disabled: true, button_options: { data: { confirm: _('USER WILL BE BLOCKED! Are you sure?'), method: :put } }) do + = _('Already blocked') + - else + = render Pajamas::ButtonComponent.new(href: block_admin_user_path(user), block: true, button_options: { data: { confirm: _('USER WILL BE BLOCKED! Are you sure?'), method: :put } }) do + = _('Block user') + - if user.trusted? + = render Pajamas::ButtonComponent.new(href: untrust_admin_user_path(user), block: true, button_options: { data: { confirm: _('USER WILL NOT BE ALLOWED TO CREATE POSSIBLE SPAM! Are you sure?'), method: :put } }) do + = _('Untrust user') + - else + = render Pajamas::ButtonComponent.new(href: trust_admin_user_path(user), block: true, button_options: { data: { confirm: _('USER WILL BE ALLOWED TO CREATE POSSIBLE SPAM! Are you sure?'), method: :put } }) do + = _('Trust user') + = render Pajamas::ButtonComponent.new(href: [:admin, abuse_report], block: true, button_options: { data: { remote: true, method: :delete }, class: "js-remove-tr" }) do + = _('Remove report') diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml index 6aed8508a6a48fb50bb4277c39a5f19108876832..bcd09a55ac1bf239d5cf4086a85241a830e6bb4c 100644 --- a/app/views/admin/spam_logs/_spam_log.html.haml +++ b/app/views/admin/spam_logs/_spam_log.html.haml @@ -53,6 +53,18 @@ - 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/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml index 213d58479860dd380fa151957e547e68f11dcc87..b28e0e1a113ac9ec3db05acd0588f1c29344aa2d 100644 --- a/app/views/admin/users/_users.html.haml +++ b/app/views/admin/users/_users.html.haml @@ -44,6 +44,9 @@ = gl_tab_link_to admin_users_path(filter: "wop"), { item_active: active_when(params[:filter] == 'wop'), class: 'gl-border-0!' } do = s_('AdminUsers|Without projects') = gl_tab_counter_badge(limited_counter_with_delimiter(User.without_projects)) + = gl_tab_link_to admin_users_path(filter: "trusted"), { item_active: active_when(params[:filter] == 'trusted'), class: 'gl-border-0!' } do + = s_('AdminUsers|Trusted') + = gl_tab_counter_badge(limited_counter_with_delimiter(User.trusted)) .nav-controls = render_if_exists 'admin/users/admin_email_users' = render_if_exists 'admin/users/admin_export_user_permissions' 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/locale/gitlab.pot b/locale/gitlab.pot index 634e0ebcecfe901e2627385edac102b11603cc92..adf7ce7fe4b3285183af7b6abe46b8e73af3b6cb 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2233,6 +2233,12 @@ msgstr "" msgid "AbuseReportEvent|Successfully scheduled the user for deletion and closed the report" msgstr "" +msgid "AbuseReportEvent|Successfully trusted the user" +msgstr "" + +msgid "AbuseReportEvent|Successfully trusted the user and closed the report" +msgstr "" + msgid "AbuseReports|%{reportedUser} reported for %{category} by %{count} users" msgstr "" @@ -2302,6 +2308,9 @@ msgstr "" msgid "AbuseReport|Confirmed spam" msgstr "" +msgid "AbuseReport|Confirmed trusted user" +msgstr "" + msgid "AbuseReport|Confirmed violation of a copyright or a trademark" msgstr "" @@ -2416,6 +2425,9 @@ msgstr "" msgid "AbuseReport|Tier" msgstr "" +msgid "AbuseReport|Trust user" +msgstr "" + msgid "AbuseReport|Verification" msgstr "" @@ -3808,6 +3820,9 @@ msgstr "" msgid "AdminUsers|Admins" msgstr "" +msgid "AdminUsers|Allow user %{username} to create possible spam?" +msgstr "" + msgid "AdminUsers|An error occurred while fetching this user's contributions, and the request cannot return the number of issues, merge requests, groups, and projects linked to this user. If you proceed with deleting the user, all their contributions will still be deleted." msgstr "" @@ -3910,6 +3925,9 @@ msgstr "" msgid "AdminUsers|Delete user and contributions" msgstr "" +msgid "AdminUsers|Disallow user %{username} to create possible spam?" +msgstr "" + msgid "AdminUsers|Export permissions as CSV (max 100,000 users)" msgstr "" @@ -4024,6 +4042,9 @@ msgstr "" msgid "AdminUsers|The maximum compute minutes that jobs in this namespace can use on shared runners each month. Set 0 for unlimited. Set empty to inherit the global setting of %{minutes}" msgstr "" +msgid "AdminUsers|The user can create issues, notes, snippets, and merge requests without being blocked." +msgstr "" + msgid "AdminUsers|The user can't access git repositories." msgstr "" @@ -4054,6 +4075,12 @@ msgstr "" msgid "AdminUsers|To confirm, type %{username}." msgstr "" +msgid "AdminUsers|Trust user" +msgstr "" + +msgid "AdminUsers|Trusted" +msgstr "" + msgid "AdminUsers|Unban user" msgstr "" @@ -4069,6 +4096,9 @@ msgstr "" msgid "AdminUsers|Unlock user %{username}?" msgstr "" +msgid "AdminUsers|Untrust user" +msgstr "" + msgid "AdminUsers|User administration" msgstr "" @@ -4099,6 +4129,9 @@ msgstr "" msgid "AdminUsers|What does this mean?" msgstr "" +msgid "AdminUsers|When allowed to create possible spam:" +msgstr "" + msgid "AdminUsers|When banned:" msgstr "" @@ -4117,6 +4150,9 @@ msgstr "" msgid "AdminUsers|You are about to permanently delete the user %{username}. This will delete all issues, merge requests, groups, and projects linked to them. To avoid data loss, consider using the %{strongStart}Block user%{strongEnd} feature instead. After you %{strongStart}Delete user%{strongEnd}, you cannot undo this action or recover the data." msgstr "" +msgid "AdminUsers|You can allow possible spam in the future if necessary." +msgstr "" + msgid "AdminUsers|You can always block their account again if needed." msgstr "" @@ -4132,6 +4168,9 @@ msgstr "" msgid "AdminUsers|You can ban their account in the future if necessary." msgstr "" +msgid "AdminUsers|You can disallow creating possible spam in the future." +msgstr "" + msgid "AdminUsers|You can unban their account in the future. Their data remains intact." msgstr "" @@ -18731,6 +18770,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 "" @@ -27324,6 +27366,9 @@ msgstr "" msgid "Join your team on GitLab and contribute to an existing project" msgstr "" +msgid "Joined %{time_ago}" +msgstr "" + msgid "Joined %{user_created_time}" msgstr "" @@ -39237,6 +39282,9 @@ msgstr "" msgid "Remove priority" msgstr "" +msgid "Remove report" +msgstr "" + msgid "Remove reviewer" msgstr "" @@ -39261,6 +39309,9 @@ msgstr "" msgid "Remove user" msgstr "" +msgid "Remove user & report" +msgstr "" + msgid "Remove user from group" msgstr "" @@ -39540,6 +39591,9 @@ msgstr "" msgid "Reported %{timeAgo} by %{reportedBy}" msgstr "" +msgid "Reported by %{reporter}" +msgstr "" + msgid "Reporter" msgstr "" @@ -45971,6 +46025,9 @@ msgstr "" msgid "Successfully synced %{synced_timeago}." msgstr "" +msgid "Successfully trusted" +msgstr "" + msgid "Successfully unbanned" msgstr "" @@ -45983,6 +46040,9 @@ msgstr "" msgid "Successfully unlocked" msgstr "" +msgid "Successfully untrusted" +msgstr "" + msgid "Successfully updated %{last_updated_timeago}." msgstr "" @@ -49947,6 +50007,9 @@ msgstr "" msgid "Trigger|Trigger description" msgstr "" +msgid "Trust user" +msgstr "" + msgid "Trusted" msgstr "" @@ -50109,9 +50172,18 @@ msgstr "" msgid "USER %{user_name} WILL BE REMOVED! Are you sure?" msgstr "" +msgid "USER %{user} WILL BE REMOVED! Are you sure?" +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 "UTC" msgstr "" @@ -50451,6 +50523,9 @@ msgstr "" msgid "Untitled" msgstr "" +msgid "Untrust user" +msgstr "" + msgid "Unused" msgstr "" diff --git a/spec/factories/users.rb b/spec/factories/users.rb index d61d5cc2d78f9bb9c60b7f75fdbfbcb209ebf065..de2b5159fe710743273ef3b2dd2628a2765e3fe0 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -48,6 +48,15 @@ after(:build) { |user, _| user.ban! } end + trait :trusted do + after(:create) do |user, _| + user.custom_attributes.create!( + key: UserCustomAttribute::TRUSTED_BY, + value: "placeholder" + ) + end + end + trait :ldap_blocked do after(:build) { |user, _| user.ldap_block! } end diff --git a/spec/features/admin/admin_browse_spam_logs_spec.rb b/spec/features/admin/admin_browse_spam_logs_spec.rb index c272a8630b76deb3987a4f66e13d262ccb8e4bb0..9fab4f545c292b8d39097766b3ead1444760f2a8 100644 --- a/spec/features/admin/admin_browse_spam_logs_spec.rb +++ b/spec/features/admin/admin_browse_spam_logs_spec.rb @@ -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 diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index ca08bc9e577da3c03a00262943dbb7e808bbf081..9ab5b1fd3bb8758cbc7ee70f1005d2bac377746b 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -82,4 +82,12 @@ end end end + + it 'does not perform N+1 queries' do + control_queries = ActiveRecord::QueryRecorder.new { visit admin_users_path } + + expect { create(:user) }.to change { User.count }.by(1) + + expect { visit admin_users_path }.not_to exceed_query_limit(control_queries) + end end diff --git a/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json b/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json index 44d8e48a972c485fbc354521e5361a6091254f5c..61472b273e13422eb6ba7b28572daba32d316ae3 100644 --- a/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json +++ b/spec/fixtures/api/schemas/entities/admin_users_data_attributes_paths.json @@ -1,19 +1,51 @@ { "type": "object", "properties": { - "edit": { "type": "string" }, - "approve": { "type": "string" }, - "reject": { "type": "string" }, - "unblock": { "type": "string" }, - "block": { "type": "string" }, - "deactivate": { "type": "string" }, - "activate": { "type": "string" }, - "unlock": { "type": "string" }, - "delete": { "type": "string" }, - "delete_with_contributions": { "type": "string" }, - "admin_user": { "type": "string" }, - "ban": { "type": "string" }, - "unban": { "type": "string" } + "edit": { + "type": "string" + }, + "approve": { + "type": "string" + }, + "reject": { + "type": "string" + }, + "unblock": { + "type": "string" + }, + "block": { + "type": "string" + }, + "deactivate": { + "type": "string" + }, + "activate": { + "type": "string" + }, + "unlock": { + "type": "string" + }, + "delete": { + "type": "string" + }, + "delete_with_contributions": { + "type": "string" + }, + "admin_user": { + "type": "string" + }, + "ban": { + "type": "string" + }, + "unban": { + "type": "string" + }, + "trust": { + "type": "string" + }, + "untrust": { + "type": "string" + } }, "required": [ "edit", @@ -28,7 +60,9 @@ "delete_with_contributions", "admin_user", "ban", - "unban" + "unban", + "trust", + "untrust" ], "additionalProperties": false } diff --git a/spec/frontend/admin/users/constants.js b/spec/frontend/admin/users/constants.js index d341eb03b1b33c6426ae1a50b65be5fffbbebdf0..39e8e51f43ca0bb9b196d3346e06c185dc1f6458 100644 --- a/spec/frontend/admin/users/constants.js +++ b/spec/frontend/admin/users/constants.js @@ -9,6 +9,8 @@ const REJECT = 'reject'; const APPROVE = 'approve'; const BAN = 'ban'; const UNBAN = 'unban'; +const TRUST = 'trust'; +const UNTRUST = 'untrust'; export const EDIT = 'edit'; @@ -24,6 +26,8 @@ export const CONFIRMATION_ACTIONS = [ UNBAN, APPROVE, REJECT, + TRUST, + UNTRUST, ]; export const DELETE_ACTIONS = [DELETE, DELETE_WITH_CONTRIBUTIONS]; diff --git a/spec/helpers/admin/user_actions_helper_spec.rb b/spec/helpers/admin/user_actions_helper_spec.rb index 87d2308690c6175136baf0ee9c9c36034f56f174..abfdabf3413df96be4a3821f5bd4d8b30368518f 100644 --- a/spec/helpers/admin/user_actions_helper_spec.rb +++ b/spec/helpers/admin/user_actions_helper_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe Admin::UserActionsHelper do +RSpec.describe Admin::UserActionsHelper, feature_category: :user_management do describe '#admin_actions' do let_it_be(:current_user) { build(:user) } @@ -29,13 +29,33 @@ context 'the user is a standard user' do let_it_be(:user) { create(:user) } - it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete", "delete_with_contributions") } + it do + is_expected.to contain_exactly( + "edit", + "block", + "ban", + "deactivate", + "delete", + "delete_with_contributions", + "trust" + ) + end end context 'the user is an admin user' do let_it_be(:user) { create(:user, :admin) } - it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete", "delete_with_contributions") } + it do + is_expected.to contain_exactly( + "edit", + "block", + "ban", + "deactivate", + "delete", + "delete_with_contributions", + "trust" + ) + end end context 'the user is blocked by LDAP' do @@ -59,7 +79,16 @@ context 'the user is deactivated' do let_it_be(:user) { create(:user, :deactivated) } - it { is_expected.to contain_exactly("edit", "block", "ban", "activate", "delete", "delete_with_contributions") } + it do + is_expected.to contain_exactly( + "edit", + "block", + "ban", + "activate", + "delete", + "delete_with_contributions" + ) + end end context 'the user is locked' do @@ -77,7 +106,8 @@ "deactivate", "unlock", "delete", - "delete_with_contributions" + "delete_with_contributions", + "trust" ) } end @@ -88,6 +118,21 @@ it { is_expected.to contain_exactly("edit", "unban", "delete", "delete_with_contributions") } end + context 'the user is trusted' do + let_it_be(:user) { create(:user, :trusted) } + + it do + is_expected.to contain_exactly("edit", + "block", + "deactivate", + "ban", + "delete", + "delete_with_contributions", + "untrust" + ) + end + end + context 'the current_user does not have permission to delete the user' do let_it_be(:user) { build(:user) } @@ -95,7 +140,7 @@ allow(helper).to receive(:can?).with(current_user, :destroy_user, user).and_return(false) end - it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate") } + it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "trust") } end context 'the user is a sole owner of a group' do @@ -106,7 +151,7 @@ group.add_owner(user) end - it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete_with_contributions") } + it { is_expected.to contain_exactly("edit", "block", "ban", "deactivate", "delete_with_contributions", "trust") } end context 'the user is a bot' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f286d6783603143ac6afa6f5f3b6977806dd57af..6e56c9b335629b8f8123b8531434adce88421741 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -6116,25 +6116,23 @@ def add_user(access) end end - describe '#allow_possible_spam?' do + describe '#trusted?' do context 'when no custom attribute is set' do - it 'is false' do - expect(user.allow_possible_spam?).to be_falsey + it 'is falsey' do + expect(user.trusted?).to be_falsey end end context 'when the custom attribute is set' do before do - user.custom_attributes.upsert_custom_attributes( - [{ - user_id: user.id, - key: UserCustomAttribute::ALLOW_POSSIBLE_SPAM, - value: "test" - }]) + user.custom_attributes.create!( + key: UserCustomAttribute::TRUSTED_BY, + value: "test" + ) end - it '#allow_possible_spam? is true' do - expect(user.allow_possible_spam?).to be_truthy + it 'is truthy' do + expect(user.trusted?).to be_truthy end end end diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb index e525d615b50311c6e714d2d15501ad1956afc344..ef3a1c1375a535973e07d9716fa0892165350bad 100644 --- a/spec/requests/admin/users_controller_spec.rb +++ b/spec/requests/admin/users_controller_spec.rb @@ -74,4 +74,24 @@ 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 + 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 + end end diff --git a/spec/services/admin/abuse_reports/moderate_user_service_spec.rb b/spec/services/admin/abuse_reports/moderate_user_service_spec.rb index 6e8a59f4e49f59080c465d5b536f95719bfc6b9c..08fbd734c9fc177340ee93e205421c806ef4eb67 100644 --- a/spec/services/admin/abuse_reports/moderate_user_service_spec.rb +++ b/spec/services/admin/abuse_reports/moderate_user_service_spec.rb @@ -193,6 +193,43 @@ end end + describe 'when trusting the user' do + let(:action) { 'trust_user' } + + it 'calls the Users::TrustService method' do + expect_next_instance_of(Users::TrustService, admin) do |service| + expect(service).to receive(:execute).with(abuse_report.user).and_return(status: :success) + end + + subject + end + + context 'when not closing the report' do + let(:close) { false } + + it_behaves_like 'does not close the report' + it_behaves_like 'records an event', action: 'trust_user' + end + + context 'when closing the report' do + it_behaves_like 'closes the report' + it_behaves_like 'records an event', action: 'trust_user_and_close_report' + end + + context 'when trusting the user fails' do + before do + allow_next_instance_of(Users::TrustService) do |service| + allow(service).to receive(:execute).with(abuse_report.user) + .and_return(status: :error, message: 'Trusting the user failed') + end + end + + it_behaves_like 'returns an error response', 'Trusting the user failed' + it_behaves_like 'does not close the report' + it_behaves_like 'does not record an event' + end + end + describe 'when only closing the report' do let(:action) { '' } diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb index 70f43d82ead04b5d063b12296a55b735fa08a1b1..909f0d5e784dd5cad10f598307bbb371d7eae7cc 100644 --- a/spec/services/spam/spam_verdict_service_spec.rb +++ b/spec/services/spam/spam_verdict_service_spec.rb @@ -141,7 +141,7 @@ UserCustomAttribute.upsert_custom_attributes( [{ user_id: user.id, - key: 'allow_possible_spam', + key: 'trusted_by', value: 'does not matter' }] ) diff --git a/spec/services/users/allow_possible_spam_service_spec.rb b/spec/services/users/trust_service_spec.rb similarity index 80% rename from spec/services/users/allow_possible_spam_service_spec.rb rename to spec/services/users/trust_service_spec.rb index 53618f0c8e961463b0e82b1fe5644e4b35758abe..1f71992ce9b38e78c8de948301fdd44294a578de 100644 --- a/spec/services/users/allow_possible_spam_service_spec.rb +++ b/spec/services/users/trust_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Users::AllowPossibleSpamService, feature_category: :user_management do +RSpec.describe Users::TrustService, feature_category: :user_management do let_it_be(:current_user) { create(:admin) } subject(:service) { described_class.new(current_user) } @@ -18,7 +18,7 @@ operation user.reload - expect(user.custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM)).to be_present + expect(user.custom_attributes.by_key(UserCustomAttribute::TRUSTED_BY)).to be_present end end end diff --git a/spec/services/users/disallow_possible_spam_service_spec.rb b/spec/services/users/untrust_service_spec.rb similarity index 80% rename from spec/services/users/disallow_possible_spam_service_spec.rb rename to spec/services/users/untrust_service_spec.rb index 32a47e05525a7b5f66659aa3711503d2f17dd5fa..bfa81bb233a298f22a7e13989400c3e1c5a7068a 100644 --- a/spec/services/users/disallow_possible_spam_service_spec.rb +++ b/spec/services/users/untrust_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Users::DisallowPossibleSpamService, feature_category: :user_management do +RSpec.describe Users::UntrustService, feature_category: :user_management do let_it_be(:current_user) { create(:admin) } subject(:service) { described_class.new(current_user) } @@ -16,14 +16,14 @@ UserCustomAttribute.upsert_custom_attributes( [{ user_id: user.id, - key: :allow_possible_spam, + key: UserCustomAttribute::TRUSTED_BY, value: 'not important' }] ) end it 'updates the custom attributes', :aggregate_failures do - expect(user.custom_attributes.by_key(UserCustomAttribute::ALLOW_POSSIBLE_SPAM)).to be_present + expect(user.custom_attributes.by_key(UserCustomAttribute::TRUSTED_BY)).to be_present operation user.reload