mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-03 00:20:04 +08:00
This commit introduces a `s3_enable_access_control_tags` site setting which, when enabled, adds a `discourse:acl` tag with values `public` or `private` to S3 objects created by the application. The presence of the tags on S3 objects enables bucket administrators to implement tag-based access control policies, providing an alternative to object ACLs which AWS now discourages. The `discourse:acl` tag can be customized via the `s3_access_control_tag_key ` site setting. Values for `public` and `private` can also be customized via the `s3_access_control_tag_public_value` and `s3_access_control_tag_private_value ` site settings respectively. ### Reviewer Notes To test it locally, run the following commands in your working discourse directory: 1. `script/install_minio_binaries.rb` 2. Start a local minio server by running: `bundle exec rails runner script/local_minio_s3.rb` 3. bundle exec rails runner "SiteSetting.enable_s3_uploads = true" 5. Start your development rails server with the following environment variables: `DISCOURSE_ENABLE_S3_UPLOADS=true DISCOURSE_S3_ENABLE_ACCESS_CONTROL_TAGS=true DISCOURSE_BACKUP_LOCATION=s3`
260 lines
8.1 KiB
Ruby
260 lines
8.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe UploadRecovery do
|
|
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
|
|
|
|
let(:upload) do
|
|
UploadCreator.new(file_from_fixtures("smallest.png"), "logo.png").create_for(user.id)
|
|
end
|
|
|
|
let(:upload2) do
|
|
UploadCreator.new(file_from_fixtures("small.pdf", "pdf"), "some.pdf").create_for(user.id)
|
|
end
|
|
|
|
let(:post) do
|
|
Fabricate(:post, raw: "", user: user).tap(&:link_post_uploads)
|
|
end
|
|
|
|
let(:upload_recovery) { UploadRecovery.new }
|
|
|
|
before do
|
|
SiteSetting.authorized_extensions = "png|pdf"
|
|
Jobs.run_immediately!
|
|
end
|
|
|
|
after do
|
|
[upload, upload2].each do |u|
|
|
next if u
|
|
public_path = "#{Discourse.store.public_dir}#{u.url}"
|
|
|
|
[public_path, public_path.sub("uploads", "uploads/tombstone")].each do |path|
|
|
File.delete(path) if File.exist?(path)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#recover" do
|
|
describe "when given an invalid sha1" do
|
|
it "does nothing" do
|
|
upload_recovery.expects(:recover_from_local).never
|
|
|
|
post.update!(raw: "")
|
|
|
|
upload_recovery.recover
|
|
|
|
post.update!(
|
|
raw: "<a href=#{"/uploads/test/original/3X/a/6%0A/#{upload.sha1}.png"}>test</a>",
|
|
)
|
|
|
|
upload_recovery.recover
|
|
end
|
|
end
|
|
|
|
it "accepts a custom ActiveRecord relation" do
|
|
post.update!(updated_at: 2.days.ago)
|
|
upload.destroy!
|
|
|
|
upload_recovery.expects(:recover_from_local).never
|
|
upload_recovery.recover(Post.where("updated_at >= ?", 1.day.ago))
|
|
end
|
|
|
|
describe "for a missing attachment" do
|
|
let(:post) { Fabricate(:post, raw: <<~SQL, user: user).tap(&:link_post_uploads) }
|
|
<a class="attachment" href="#{upload2.url}">some.pdf</a>
|
|
<a>blank</a>
|
|
SQL
|
|
|
|
it "recovers the attachment" do
|
|
expect do upload2.destroy! end.to change { post.reload.uploads.count }.from(1).to(0)
|
|
|
|
expect do upload_recovery.recover end.to change { post.reload.uploads.count }.from(0).to(1)
|
|
|
|
expect(File.read(Discourse.store.path_for(post.uploads.first))).to eq(
|
|
File.read(file_from_fixtures("small.pdf", "pdf")),
|
|
)
|
|
end
|
|
end
|
|
|
|
it "recovers uploads and attachments" do
|
|
stub_request(:get, "http://test.localhost#{upload.url}").to_return(status: 200)
|
|
|
|
expect do upload.destroy! end.to change { post.reload.uploads.count }.from(1).to(0)
|
|
|
|
expect do upload_recovery.recover end.to change { post.reload.uploads.count }.from(0).to(1)
|
|
|
|
expect(File.read(Discourse.store.path_for(post.uploads.first))).to eq(
|
|
File.read(file_from_fixtures("smallest.png")),
|
|
)
|
|
end
|
|
|
|
describe "S3 store" do
|
|
before do
|
|
setup_s3
|
|
stub_s3_store
|
|
end
|
|
|
|
it "recovers the upload" do
|
|
expect do upload.destroy! end.to change { post.reload.uploads.count }.from(1).to(0)
|
|
|
|
original_key = Discourse.store.get_path_for_upload(upload)
|
|
tombstone_key = original_key.sub("original", "tombstone/original")
|
|
|
|
tombstone_copy = stub
|
|
tombstone_copy.expects(:key).returns(tombstone_key)
|
|
|
|
Discourse.store.s3_helper.expects(:list).with("original").returns([])
|
|
|
|
Discourse
|
|
.store
|
|
.s3_helper
|
|
.expects(:list)
|
|
.with("#{FileStore::S3Store::TOMBSTONE_PREFIX}original")
|
|
.returns([tombstone_copy])
|
|
|
|
Discourse
|
|
.store
|
|
.s3_helper
|
|
.expects(:copy)
|
|
.with(
|
|
tombstone_key,
|
|
original_key,
|
|
options: {
|
|
acl: FileStore::S3Store::CANNED_ACL_PUBLIC_READ,
|
|
},
|
|
)
|
|
|
|
FileHelper.expects(:download).returns(file_from_fixtures("smallest.png"))
|
|
stub_request(:get, upload.url).to_return(body: file_from_fixtures("smallest.png"))
|
|
|
|
expect do upload_recovery.recover end.to change { post.reload.uploads.count }.from(0).to(1)
|
|
end
|
|
|
|
describe "when the upload exists but its file is missing" do
|
|
it "recovers the file" do
|
|
upload.verification_status = Upload.verification_statuses[:invalid_etag]
|
|
upload.save!
|
|
|
|
original_key = Discourse.store.get_path_for_upload(upload)
|
|
tombstone_key = original_key.sub("original", "tombstone/original")
|
|
|
|
tombstone_copy = stub
|
|
tombstone_copy.expects(:key).returns(tombstone_key)
|
|
|
|
Discourse.store.s3_helper.expects(:list).with("original").returns([])
|
|
|
|
Discourse
|
|
.store
|
|
.s3_helper
|
|
.expects(:list)
|
|
.with("#{FileStore::S3Store::TOMBSTONE_PREFIX}original")
|
|
.returns([tombstone_copy])
|
|
|
|
Discourse
|
|
.store
|
|
.s3_helper
|
|
.expects(:copy)
|
|
.with(
|
|
tombstone_key,
|
|
original_key,
|
|
options: {
|
|
acl: FileStore::S3Store::CANNED_ACL_PUBLIC_READ,
|
|
},
|
|
)
|
|
|
|
expect do upload_recovery.recover end.to_not change {
|
|
[post.reload.uploads.count, Upload.count]
|
|
}
|
|
end
|
|
|
|
it "does not create a duplicate upload when secure uploads are enabled" do
|
|
SiteSetting.secure_uploads = true
|
|
upload.verification_status = Upload.verification_statuses[:invalid_etag]
|
|
upload.save!
|
|
|
|
original_key = Discourse.store.get_path_for_upload(upload)
|
|
tombstone_key = original_key.sub("original", "tombstone/original")
|
|
|
|
tombstone_copy = stub
|
|
tombstone_copy.expects(:key).returns(tombstone_key)
|
|
|
|
Discourse.store.s3_helper.expects(:list).with("original").returns([])
|
|
Discourse
|
|
.store
|
|
.s3_helper
|
|
.expects(:list)
|
|
.with("#{FileStore::S3Store::TOMBSTONE_PREFIX}original")
|
|
.returns([tombstone_copy])
|
|
Discourse
|
|
.store
|
|
.s3_helper
|
|
.expects(:copy)
|
|
.with(
|
|
tombstone_key,
|
|
original_key,
|
|
options: {
|
|
acl: FileStore::S3Store::CANNED_ACL_PUBLIC_READ,
|
|
},
|
|
)
|
|
|
|
expect do upload_recovery.recover end.to_not change {
|
|
[post.reload.uploads.count, Upload.count]
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "image tag" do
|
|
let(:post) { Fabricate(:post, raw: <<~SQL, user: user).tap(&:link_post_uploads) }
|
|
<img src='#{upload.url}'>
|
|
SQL
|
|
|
|
it "recovers the upload" do
|
|
stub_request(:get, "http://test.localhost#{upload.url}").to_return(status: 200)
|
|
|
|
expect do upload.destroy! end.to change { post.reload.uploads.count }.from(1).to(0)
|
|
|
|
expect do upload_recovery.recover end.to change { post.reload.uploads.count }.from(0).to(1)
|
|
|
|
expect(File.read(Discourse.store.path_for(post.uploads.first))).to eq(
|
|
File.read(file_from_fixtures("smallest.png")),
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "image markdown" do
|
|
let(:post) { Fabricate(:post, raw: <<~SQL, user: user).tap(&:link_post_uploads) }
|
|

|
|
SQL
|
|
|
|
it "recovers the upload" do
|
|
stub_request(:get, "http://test.localhost#{upload.url}").to_return(status: 200)
|
|
|
|
expect do upload.destroy! end.to change { post.reload.uploads.count }.from(1).to(0)
|
|
|
|
expect do upload_recovery.recover end.to change { post.reload.uploads.count }.from(0).to(1)
|
|
|
|
expect(File.read(Discourse.store.path_for(post.uploads.first))).to eq(
|
|
File.read(file_from_fixtures("smallest.png")),
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "bbcode" do
|
|
let(:post) { Fabricate(:post, raw: <<~SQL, user: user).tap(&:link_post_uploads) }
|
|
[img]#{upload.url}[/img]
|
|
SQL
|
|
|
|
it "recovers the upload" do
|
|
stub_request(:get, "http://test.localhost#{upload.url}").to_return(status: 200)
|
|
|
|
expect do upload.destroy! end.to change { post.reload.uploads.count }.from(1).to(0)
|
|
|
|
expect do upload_recovery.recover end.to change { post.reload.uploads.count }.from(0).to(1)
|
|
|
|
expect(File.read(Discourse.store.path_for(post.uploads.first))).to eq(
|
|
File.read(file_from_fixtures("smallest.png")),
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|