Skip to content

Commit

Permalink
Add basic admin UI for emergency rules
Browse files Browse the repository at this point in the history
  • Loading branch information
ClearlyClaire committed Jan 4, 2024
1 parent 84f8cb9 commit f44b28a
Show file tree
Hide file tree
Showing 16 changed files with 253 additions and 0 deletions.
27 changes: 27 additions & 0 deletions app/controllers/admin/emergency_rules_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Admin
class EmergencyRulesController < BaseController
before_action :set_rule, except: [:index]

def index
authorize [:emergency, :rule], :index?

@rules = Emergency::Rule.all.to_a
end

def deactivate
authorize @rule, :deactivate?
# TODO: log?
@rule.deactivate!

redirect_to admin_emergency_rules_path, notice: I18n.t('admin.emergency_rules.deactivated_msg')
end

private

def set_rule
@rule = Emergency::Rule.find(params[:id])
end
end
end
8 changes: 8 additions & 0 deletions app/helpers/admin/dashboard_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,12 @@ def relevant_account_timestamp(account)

content_tag(:time, l(timestamp), class: 'time-ago', datetime: timestamp.iso8601, title: l(timestamp))
end

def emergency_rule_notice
return unless Emergency::Rule.any_active?

content_tag(:div, class: 'flash-message warning') do
I18n.t('admin.emergency_rules.notice_html', path: admin_emergency_rules_path).html_safe
end
end
end
15 changes: 15 additions & 0 deletions app/policies/emergency/rule_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class Emergency::RulePolicy < ApplicationPolicy
def create?
false # TODO
end

def index?
role.can?(:manage_reports, :view_audit_log, :manage_users, :manage_invites, :manage_taxonomies, :manage_federation, :manage_blocks)
end

def deactivate?
role.can?(:manage_reports, :view_audit_log, :manage_users, :manage_invites, :manage_taxonomies, :manage_federation, :manage_blocks)
end
end
2 changes: 2 additions & 0 deletions app/views/admin/accounts/index.html.haml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
- content_for :page_title do
= t('admin.accounts.title')

= emergency_rule_notice

= form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do
.filters
.filter-subset.filter-subset--with-select
Expand Down
2 changes: 2 additions & 0 deletions app/views/admin/dashboard/index.html.haml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
= emergency_rule_notice

- content_for :page_title do
= t('admin.dashboard.title')

Expand Down
24 changes: 24 additions & 0 deletions app/views/admin/emergency_rules/_emergency_rule.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.filters-list__item
.filters-list__item__title
= emergency_rule.name

- if emergency_rule.active?
.expiration
- if can?(:deactivate, emergency_rule)
= link_to t('admin.emergency_rules.index.deactivate'), deactivate_admin_emergency_rule_path(emergency_rule.id), method: :post, class: 'button button-tertiary button--destructive'
- else
= t('admin.emergency_rules.active')

.filters-list__item__permissions
%ul.permissions-list
= render partial: 'trigger', collection: emergency_rule.triggers
= render partial: 'rate_limit_action', collection: emergency_rule.rate_limit_actions
= render partial: 'setting_override_action', collection: emergency_rule.setting_override_actions

.announcements-list__item__action-bar
.announcements-list__item__meta
-# TODO: history
- if emergency_rule.duration.present?
= t('admin.emergency_rules.expires_after_duration', duration: distance_of_time_in_words(Time.at(0).utc, Time.at(emergency_rule.duration).utc))
- else
= t('admin.emergency_rules.no_expiration')
8 changes: 8 additions & 0 deletions app/views/admin/emergency_rules/_rate_limit_action.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
%li.permissions-list__item
.permissions-list__item__icon
= fa_icon('gavel')
.permissions-list__item__text
.permissions-list__item__text__title
= rate_limit_action.new_users_only? ? t('admin.emergency_rules.rate_limit_actions.new_users_title') : t('admin.emergency_rules.rate_limit_actions.title')
.permissions-list__item__text__type
= rate_limit_action.new_users_only? ? t('admin.emergency_rules.rate_limit_actions.new_users_description') : t('admin.emergency_rules.rate_limit_actions.description')
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
%li.permissions-list__item
.permissions-list__item__icon
= fa_icon('gavel')
.permissions-list__item__text
.permissions-list__item__text__title
= t("admin.emergency_rules.setting_override_actions.#{setting_override_action.setting}.title_#{setting_override_action.value}")
.permissions-list__item__text__type
= t('admin.emergency_rules.setting_override_actions.description')
9 changes: 9 additions & 0 deletions app/views/admin/emergency_rules/_trigger.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
%li.permissions-list__item
.permissions-list__item__icon
= fa_icon('cogs')
.permissions-list__item__text
.permissions-list__item__text__title
= t("admin.emergency_rules.triggers.title.#{trigger.event}")
.permissions-list__item__text__type
- period = t("admin.emergency_rules.triggers.periods.#{trigger.duration_bucket}")
= t('admin.emergency_rules.triggers.description', threshold: trigger.threshold, period: period)
13 changes: 13 additions & 0 deletions app/views/admin/emergency_rules/index.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
- content_for :page_title do
= t('admin.emergency_rules.index.title')

- content_for :heading_actions do
= link_to t('admin.emergency_rules.index.add'), new_admin_emergency_rule_path, class: 'button' if can?(:create, [:emergency, :rule])

%p.lead= t('admin.emergency_rules.index.preamble')

- if @rules.empty?
.muted-hint.center-text= t 'admin.emergency_rules.index.empty'
- else
.applications-list
= render partial: 'emergency_rule', collection: @rules
2 changes: 2 additions & 0 deletions app/views/admin/reports/index.html.haml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
- content_for :page_title do
= t('admin.reports.title')

= emergency_rule_notice

.filters
.filter-subset
%strong= t('admin.reports.status')
Expand Down
1 change: 1 addition & 0 deletions config/i18n-tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ ignore_unused:
- 'admin.action_logs.actions.*'
- 'admin.reports.summary.action_preambles.*'
- 'admin.reports.summary.actions.*'
- 'admin.emergency_rules.setting_override_actions.*.title_*'
- 'admin_mailer.new_appeal.actions.*'
- 'statuses.attached.*'
- 'move_handler.carry_{mutes,blocks}_over_text'
Expand Down
34 changes: 34 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,40 @@ en:
resolved_dns_records_hint_html: The domain name resolves to the following MX domains, which are ultimately responsible for accepting e-mail. Blocking an MX domain will block sign-ups from any e-mail address which uses the same MX domain, even if the visible domain name is different. <strong>Be careful not to block major e-mail providers.</strong>
resolved_through_html: Resolved through %{domain}
title: Blocked e-mail domains
emergency_rules:
active: Active
deactivated_msg: Successfully deactivated emergency rule
expires_after_duration: Deactivates %{duration} after most recent trigger
index:
add: Add new rule
deactivate: Deactivate
empty: There are currently no emergency rules set up on this server.
preamble: Emergency rules are configurable automated rules meant to give moderators some time to breath when the server is facing an unusual surge of activity.
title: Emergency rules
no_expiration: Needs to be manually disabled once triggered
notice_html: An <a href="%{path}">emergency rule</a> is currently enabled.
rate_limit_actions:
description: All local accounts get heavily rate-limited when this rule is active
new_users_description: Accounts confirmed around or after this rule got triggered get heavily rate-limited
new_users_title: Heavily rate-limit new users
title: Heavily rate-limit users
setting_override_actions:
captcha_enabled:
title_true: Enable CAPTCHA
description: Overrides the server settings when this rule is active
registrations_mode:
title_approved: Switch registrations to approval-based
title_none: Close registrations
triggers:
description: Triggers when reaching %{threshold} per %{period}
periods:
day: day
hour: hour
minute: minute
title:
local:confirmations: New account confirmations
local:posts: New posts
local:signups: New registrations
export_domain_allows:
new:
title: Import domain allows
Expand Down
1 change: 1 addition & 0 deletions config/navigation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
s.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_path, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.can?(:manage_blocks) }
s.item :ip_blocks, safe_join([fa_icon('ban fw'), t('admin.ip_blocks.title')]), admin_ip_blocks_path, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.can?(:manage_blocks) }
s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_path, if: -> { current_user.can?(:view_audit_log) }
s.item :emergency_rules, safe_join([fa_icon('shield fw'), t('admin.emergency_rules.index.title')]), admin_emergency_rules_path, highlights_on: %r{/admin/emergency_rules}, if: -> { current_user.can?(:manage_reports, :view_audit_log, :manage_users, :manage_invites, :manage_taxonomies, :manage_federation, :manage_blocks) }

Check failure on line 52 in config/navigation.rb

View workflow job for this annotation

GitHub Actions / lint

[Correctable] Layout/LineLength: Line is too long. [327/320] (https://rubystyle.guide#max-line-length)
end

n.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), nil, if: -> { current_user.can?(:view_dashboard, :manage_settings, :manage_rules, :manage_announcements, :manage_custom_emojis, :manage_webhooks, :manage_federation) && !self_destruct } do |s|
Expand Down
6 changes: 6 additions & 0 deletions config/routes/admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,10 @@
end

resources :software_updates, only: [:index]

resources :emergency_rules do
member do
post :deactivate
end
end
end
93 changes: 93 additions & 0 deletions spec/requests/admin/emergency_rules_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Admin::EmergencyRulesController do
before do
Fabricate('Emergency::Trigger')
Fabricate('Emergency::SettingOverrideAction')
Fabricate('Emergency::RateLimitAction')
end

describe 'the index route' do
context 'when not logged in' do
it 'returns HTTP forbidden' do
get '/admin/emergency_rules'

expect(response).to have_http_status(403)
end
end

context 'when logged in as a regular user' do
before do
sign_in Fabricate(:user), scope: :user
end

it 'returns HTTP forbidden' do
get '/admin/emergency_rules'

expect(response).to have_http_status(403)
end
end

context 'when logged in as a moderator' do
let(:user) { Fabricate(:user, role: UserRole.find_by!(name: 'Moderator')) }

before do
sign_in user, scope: :user
end

it 'returns HTTP success' do
get '/admin/emergency_rules'

expect(response).to have_http_status(200)
end
end
end

describe 'the deactivation route' do
let(:rule) { Fabricate('Emergency::Rule') }

before do
rule.trigger!(1.day.ago)
end

context 'when not logged in' do
it 'returns HTTP forbidden' do
post "/admin/emergency_rules/#{rule.id}/deactivate"

expect(response).to have_http_status(403)
end
end

context 'when logged in as a regular user' do
before do
sign_in Fabricate(:user), scope: :user
end

it 'returns HTTP forbidden' do
post "/admin/emergency_rules/#{rule.id}/deactivate"

expect(response).to have_http_status(403)
end
end

context 'when logged in as a moderator' do
let(:user) { Fabricate(:user, role: UserRole.find_by!(name: 'Moderator')) }

before do
sign_in user, scope: :user
end

it 'redirects' do
post "/admin/emergency_rules/#{rule.id}/deactivate"

expect(response).to redirect_to '/admin/emergency_rules'
end

it 'deactivates the rule' do
expect { post "/admin/emergency_rules/#{rule.id}/deactivate" }.to change { rule.reload.active? }.from(true).to(false)
end
end
end
end

0 comments on commit f44b28a

Please sign in to comment.