mirror of
https://github.com/discourse/discourse.git
synced 2025-04-01 20:56:10 +08:00
SECURITY: Rate limit the creation of backups
This commit is contained in:
parent
272c31023d
commit
0bd64788d2
@ -34,6 +34,14 @@ class Admin::BackupsController < Admin::AdminController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
RateLimiter.new(
|
||||||
|
current_user,
|
||||||
|
"max-backups-per-minute",
|
||||||
|
1,
|
||||||
|
1.minute,
|
||||||
|
apply_limit_to_staff: true,
|
||||||
|
).performed!
|
||||||
|
|
||||||
opts = {
|
opts = {
|
||||||
publish_to_message_bus: true,
|
publish_to_message_bus: true,
|
||||||
with_uploads: params.fetch(:with_uploads) == "true",
|
with_uploads: params.fetch(:with_uploads) == "true",
|
||||||
|
@ -5,7 +5,7 @@ require "file_store/s3_store"
|
|||||||
|
|
||||||
module BackupRestore
|
module BackupRestore
|
||||||
class Backuper
|
class Backuper
|
||||||
attr_reader :success
|
attr_reader :success, :store
|
||||||
|
|
||||||
def initialize(user_id, opts = {})
|
def initialize(user_id, opts = {})
|
||||||
@user_id = user_id
|
@user_id = user_id
|
||||||
@ -46,7 +46,6 @@ module BackupRestore
|
|||||||
rescue Exception => ex
|
rescue Exception => ex
|
||||||
log "EXCEPTION: " + ex.message
|
log "EXCEPTION: " + ex.message
|
||||||
log ex.backtrace.join("\n")
|
log ex.backtrace.join("\n")
|
||||||
@success = false
|
|
||||||
else
|
else
|
||||||
@success = true
|
@success = true
|
||||||
@backup_filename
|
@backup_filename
|
||||||
@ -55,7 +54,7 @@ module BackupRestore
|
|||||||
clean_up
|
clean_up
|
||||||
notify_user
|
notify_user
|
||||||
log "Finished!"
|
log "Finished!"
|
||||||
publish_completion(@success)
|
publish_completion
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
@ -337,12 +336,12 @@ module BackupRestore
|
|||||||
end
|
end
|
||||||
|
|
||||||
def upload_archive
|
def upload_archive
|
||||||
return unless @store.remote?
|
return unless store.remote?
|
||||||
|
|
||||||
log "Uploading archive..."
|
log "Uploading archive..."
|
||||||
content_type = MiniMime.lookup_by_filename(@backup_filename).content_type
|
content_type = MiniMime.lookup_by_filename(@backup_filename).content_type
|
||||||
archive_path = File.join(@archive_directory, @backup_filename)
|
archive_path = File.join(@archive_directory, @backup_filename)
|
||||||
@store.upload_file(@backup_filename, archive_path, content_type)
|
store.upload_file(@backup_filename, archive_path, content_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_create_hook
|
def after_create_hook
|
||||||
@ -354,16 +353,16 @@ module BackupRestore
|
|||||||
return if Rails.env.development?
|
return if Rails.env.development?
|
||||||
|
|
||||||
log "Deleting old backups..."
|
log "Deleting old backups..."
|
||||||
@store.delete_old
|
store.delete_old
|
||||||
rescue => ex
|
rescue => ex
|
||||||
log "Something went wrong while deleting old backups.", ex
|
log "Something went wrong while deleting old backups.", ex
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_user
|
def notify_user
|
||||||
return if @success && @user.id == Discourse::SYSTEM_USER_ID
|
return if success && @user.id == Discourse::SYSTEM_USER_ID
|
||||||
|
|
||||||
log "Notifying '#{@user.username}' of the end of the backup..."
|
log "Notifying '#{@user.username}' of the end of the backup..."
|
||||||
status = @success ? :backup_succeeded : :backup_failed
|
status = success ? :backup_succeeded : :backup_failed
|
||||||
|
|
||||||
logs = Discourse::Utils.logs_markdown(@logs, user: @user)
|
logs = Discourse::Utils.logs_markdown(@logs, user: @user)
|
||||||
post = SystemMessage.create_from_system_user(@user, status, logs: logs)
|
post = SystemMessage.create_from_system_user(@user, status, logs: logs)
|
||||||
@ -378,11 +377,11 @@ module BackupRestore
|
|||||||
delete_uploaded_archive
|
delete_uploaded_archive
|
||||||
remove_tar_leftovers
|
remove_tar_leftovers
|
||||||
mark_backup_as_not_running
|
mark_backup_as_not_running
|
||||||
refresh_disk_space
|
refresh_disk_space if success
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_uploaded_archive
|
def delete_uploaded_archive
|
||||||
return unless @store.remote?
|
return unless store.remote?
|
||||||
|
|
||||||
archive_path = File.join(@archive_directory, @backup_filename)
|
archive_path = File.join(@archive_directory, @backup_filename)
|
||||||
|
|
||||||
@ -396,7 +395,7 @@ module BackupRestore
|
|||||||
|
|
||||||
def refresh_disk_space
|
def refresh_disk_space
|
||||||
log "Refreshing disk stats..."
|
log "Refreshing disk stats..."
|
||||||
@store.reset_cache
|
store.reset_cache
|
||||||
rescue => ex
|
rescue => ex
|
||||||
log "Something went wrong while refreshing disk stats.", ex
|
log "Something went wrong while refreshing disk stats.", ex
|
||||||
end
|
end
|
||||||
@ -450,7 +449,7 @@ module BackupRestore
|
|||||||
@logs << "[#{timestamp}] #{message}"
|
@logs << "[#{timestamp}] #{message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def publish_completion(success)
|
def publish_completion
|
||||||
if success
|
if success
|
||||||
log("[SUCCESS]")
|
log("[SUCCESS]")
|
||||||
DiscourseEvent.trigger(:backup_complete, logs: @logs, ticket: @ticket)
|
DiscourseEvent.trigger(:backup_complete, logs: @logs, ticket: @ticket)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
RSpec.describe BackupRestore::Backuper do
|
RSpec.describe BackupRestore::Backuper do
|
||||||
|
describe "#get_parameterized_title" do
|
||||||
it "returns a non-empty parameterized title when site title contains unicode" do
|
it "returns a non-empty parameterized title when site title contains unicode" do
|
||||||
SiteSetting.title = "Ɣ"
|
SiteSetting.title = "Ɣ"
|
||||||
backuper = BackupRestore::Backuper.new(Discourse.system_user.id)
|
backuper = BackupRestore::Backuper.new(Discourse.system_user.id)
|
||||||
@ -14,6 +15,7 @@ RSpec.describe BackupRestore::Backuper do
|
|||||||
|
|
||||||
expect(backuper.send(:get_parameterized_title)).to eq("coding-horror")
|
expect(backuper.send(:get_parameterized_title)).to eq("coding-horror")
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "#notify_user" do
|
describe "#notify_user" do
|
||||||
before { freeze_time Time.zone.parse("2010-01-01 12:00") }
|
before { freeze_time Time.zone.parse("2010-01-01 12:00") }
|
||||||
@ -69,4 +71,32 @@ RSpec.describe BackupRestore::Backuper do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#run" do
|
||||||
|
subject(:run) { backup.run }
|
||||||
|
|
||||||
|
let(:backup) { described_class.new(user.id) }
|
||||||
|
let(:user) { Discourse.system_user }
|
||||||
|
let(:store) { backup.store }
|
||||||
|
|
||||||
|
before { backup.stubs(:success).returns(success) }
|
||||||
|
|
||||||
|
context "when the result isn't successful" do
|
||||||
|
let(:success) { false }
|
||||||
|
|
||||||
|
it "doesn't refresh disk stats" do
|
||||||
|
store.expects(:reset_cache).never
|
||||||
|
run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the result is successful" do
|
||||||
|
let(:success) { true }
|
||||||
|
|
||||||
|
it "refreshes disk stats" do
|
||||||
|
store.expects(:reset_cache)
|
||||||
|
run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -137,7 +137,10 @@ RSpec.describe Admin::BackupsController do
|
|||||||
|
|
||||||
describe "#create" do
|
describe "#create" do
|
||||||
context "when logged in as an admin" do
|
context "when logged in as an admin" do
|
||||||
before { sign_in(admin) }
|
before do
|
||||||
|
sign_in(admin)
|
||||||
|
BackupRestore.stubs(:backup!)
|
||||||
|
end
|
||||||
|
|
||||||
it "starts a backup" do
|
it "starts a backup" do
|
||||||
BackupRestore.expects(:backup!).with(
|
BackupRestore.expects(:backup!).with(
|
||||||
@ -149,6 +152,22 @@ RSpec.describe Admin::BackupsController do
|
|||||||
|
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "with rate limiting enabled" do
|
||||||
|
before do
|
||||||
|
RateLimiter.clear_all!
|
||||||
|
RateLimiter.enable
|
||||||
|
end
|
||||||
|
|
||||||
|
after { RateLimiter.disable }
|
||||||
|
|
||||||
|
it "is rate limited" do
|
||||||
|
post "/admin/backups.json", params: { with_uploads: false, client_id: "foo" }
|
||||||
|
post "/admin/backups.json", params: { with_uploads: false, client_id: "foo" }
|
||||||
|
|
||||||
|
expect(response).to have_http_status :too_many_requests
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples "backups creation not allowed" do
|
shared_examples "backups creation not allowed" do
|
||||||
|
Loading…
x
Reference in New Issue
Block a user