diff --git a/app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb b/app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb index f4ac05d6e..83c0170a8 100644 --- a/app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb @@ -1,5 +1,7 @@ class Api::V1::Accounts::Conversations::DirectUploadsController < ActiveStorage::DirectUploadsController include EnsureCurrentAccountHelper + include AccessTokenAuthHelper + before_action :prevent_read_only_access_token! before_action :current_account before_action :conversation diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index b7fc14e74..7fb27799f 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -36,4 +36,15 @@ module AccessTokenAuthHelper def agent_bot_accessible? BOT_ACCESSIBLE_ENDPOINTS.fetch(params[:controller], []).include?(params[:action]) end + + # Blocks read-only access tokens from reaching write endpoints that live outside + # Api::BaseController (e.g. controllers inheriting from ActiveStorage). Resolves + # the token directly since these controllers skip the Api::BaseController chain. + def prevent_read_only_access_token! + ensure_access_token + return unless @access_token&.scope == 'read_only' + + render json: { error: 'This access token is read-only and cannot perform write operations.' }, + status: :forbidden + end end diff --git a/spec/controllers/api/v1/accounts/conversations/direct_uploads_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations/direct_uploads_controller_spec.rb index 089b16b59..ad62e56b0 100644 --- a/spec/controllers/api/v1/accounts/conversations/direct_uploads_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations/direct_uploads_controller_spec.rb @@ -29,6 +29,24 @@ RSpec.describe '/api/v1/accounts/:account_id/conversations/:conversation_id/dire json_response = response.parsed_body expect(json_response['content_type']).to eq('image/png') end + + it 'rejects requests made with a read-only access token' do + read_only_token = agent.read_only_access_token + + post api_v1_account_conversation_direct_uploads_path(account_id: account.id, conversation_id: conversation.display_id), + params: { + blob: { + filename: 'avatar.png', + byte_size: '1234', + checksum: 'dsjbsdhbfif3874823mnsdbf', + content_type: 'image/png' + } + }, + headers: { api_access_token: read_only_token.token }, + as: :json + + expect(response).to have_http_status(:forbidden) + end end end end