Skip to content

Commit de12070

Browse files
authored
fix: attachment upload authorization in attachments controller (#4520)
* Enhance attachment upload authorization in attachments controller * run request specs
1 parent 9e23ddc commit de12070

3 files changed

Lines changed: 118 additions & 5 deletions

File tree

.github/workflows/feature-tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ jobs:
9898
9999
- name: Run tests
100100
id: run_tests
101-
run: bundle exec rspec spec/features
101+
run: |
102+
bundle exec rspec spec/features
103+
bundle exec rspec spec/requests
102104
103105
- uses: actions/upload-artifact@v4
104106
with:

app/controllers/avo/attachments_controller.rb

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ class AttachmentsController < ApplicationController
77
before_action :set_record, only: [:destroy, :create]
88

99
def create
10-
blob = ActiveStorage::Blob.create_and_upload! io: params[:file].to_io, filename: params[:filename]
1110
association_name = BaseResource.valid_attachment_name(@record, params[:attachment_key])
1211

13-
# If association name is present attach the blob to it
1412
if association_name
13+
return render_upload_unauthorized unless authorized_to_upload(association_name)
14+
15+
blob = ActiveStorage::Blob.create_and_upload! io: params[:file].to_io, filename: params[:filename]
1516
@record.send(association_name).attach blob
16-
# If key is present use the blob from the key else raise error
17-
elsif params[:key].blank?
17+
elsif params[:key].present?
18+
return render_upload_unauthorized unless authorized_to_trix_upload?
19+
20+
blob = ActiveStorage::Blob.create_and_upload! io: params[:file].to_io, filename: params[:filename]
21+
else
1822
raise ActionController::BadRequest.new("Could not find the attachment association for #{params[:attachment_key]} (check the `attachment_key` for this Trix field)")
1923
end
2024

@@ -63,5 +67,17 @@ def destroy
6367
def authorized_to(action)
6468
@resource.authorization.authorize_action("#{action}_#{params[:attachment_name]}?", record: @record, raise_exception: false)
6569
end
70+
71+
def authorized_to_upload(attachment_name)
72+
@resource.authorization.authorize_action("upload_#{attachment_name}?", record: @record, raise_exception: false)
73+
end
74+
75+
def authorized_to_trix_upload?
76+
@resource.authorization.authorize_action("update?", record: @record, raise_exception: false)
77+
end
78+
79+
def render_upload_unauthorized
80+
render json: {error: "Not authorized"}, status: :forbidden
81+
end
6682
end
6783
end
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,99 @@
11
require "rails_helper"
22

33
RSpec.describe "Attachments", type: :request do
4+
include ActionDispatch::TestProcess
5+
6+
let(:admin_user) do
7+
User.create!(
8+
first_name: "Admin",
9+
last_name: "User",
10+
email: "admin@example.com",
11+
password: "password",
12+
roles: {"admin" => true}
13+
)
14+
end
15+
16+
let(:post_record) do
17+
Post.create!(name: "Hello", body: "World", user: admin_user)
18+
end
19+
20+
def build_upload(content:, original_filename:, content_type:)
21+
tmp = Tempfile.new(["upload", File.extname(original_filename)])
22+
tmp.binmode
23+
tmp.write(content)
24+
tmp.rewind
25+
26+
Rack::Test::UploadedFile.new(tmp.path, content_type, original_filename: original_filename)
27+
end
28+
29+
it "denies has_one_attached upload when upload_cover_photo? is false" do
30+
sign_in admin_user
31+
32+
post_record.cover_photo.attach(
33+
io: StringIO.new("old"),
34+
filename: "old.txt",
35+
content_type: "text/plain"
36+
)
37+
38+
allow_any_instance_of(Avo::Services::AuthorizationService)
39+
.to receive(:authorize_action)
40+
.with("upload_cover_photo?", record: post_record, raise_exception: false)
41+
.and_return(false)
42+
43+
upload = build_upload(content: "new", original_filename: "new.txt", content_type: "text/plain")
44+
old_blob_id = post_record.reload.cover_photo.blob_id
45+
old_checksum = post_record.cover_photo.blob.checksum
46+
47+
expect {
48+
post "/admin/avo_api/resources/posts/#{post_record.to_param}/attachments",
49+
params: {file: upload, filename: "new.txt", attachment_key: "cover_photo"},
50+
headers: {"ACCEPT" => "application/json"}
51+
}.not_to change { ActiveStorage::Blob.count }
52+
53+
post_record.reload
54+
expect(post_record.cover_photo).to be_attached
55+
expect(post_record.cover_photo.blob_id).to eq(old_blob_id)
56+
expect(post_record.cover_photo.blob.checksum).to eq(old_checksum)
57+
58+
expect(response).to have_http_status(:forbidden)
59+
end
60+
61+
it "denies has_many_attached upload when upload_attachments? is false" do
62+
sign_in admin_user
63+
64+
allow_any_instance_of(Avo::Services::AuthorizationService)
65+
.to receive(:authorize_action)
66+
.with("upload_attachments?", record: post_record, raise_exception: false)
67+
.and_return(false)
68+
69+
upload = build_upload(content: "one", original_filename: "one.txt", content_type: "text/plain")
70+
71+
expect {
72+
post "/admin/avo_api/resources/posts/#{post_record.to_param}/attachments",
73+
params: {file: upload, filename: "one.txt", attachment_key: "attachments"},
74+
headers: {"ACCEPT" => "application/json"}
75+
}.not_to change { ActiveStorage::Blob.count }
76+
77+
expect(post_record.reload.attachments.count).to eq(0)
78+
expect(response).to have_http_status(:forbidden)
79+
end
80+
81+
it "denies key/Trix-style upload when no attachment association is resolved" do
82+
sign_in admin_user
83+
84+
allow_any_instance_of(Avo::Services::AuthorizationService)
85+
.to receive(:authorize_action)
86+
.with("update?", record: post_record, raise_exception: false)
87+
.and_return(false)
88+
89+
upload = build_upload(content: "trix", original_filename: "trix.txt", content_type: "text/plain")
90+
91+
expect {
92+
post "/admin/avo_api/resources/posts/#{post_record.to_param}/attachments",
93+
params: {file: upload, filename: "trix.txt", attachment_key: "missing_field", key: "trixKey"},
94+
headers: {"ACCEPT" => "application/json"}
95+
}.not_to change { ActiveStorage::Blob.count }
96+
97+
expect(response).to have_http_status(:forbidden)
98+
end
499
end

0 commit comments

Comments
 (0)