refactor: align conversation direct uploads with standard account auth (#15039)
Conversation attachment uploads now go through the same authentication that every other account-scoped API endpoint uses. Agents continue to attach files exactly as before, and the upload request is now tied to the agent's dashboard session instead of a separately serialized access token. Because the upload request is now authenticated, the dashboard proves the agent's session directly instead of passing `currentUser.access_token`. This keeps uploads working alongside the profile access-token changes in #14973, including on accounts where that token is serialized as empty. ## What changed - `Api::V1::Accounts::Conversations::DirectUploadsController` now runs the standard account auth stack: API access token when the `api_access_token` header is present, dashboard session (devise-token-auth) otherwise, with agent-bot tokens rejected. Previously it inherited `ActiveStorage::DirectUploadsController` directly and did not run any authentication. - `EnsureCurrentAccountHelper#ensure_current_account` now returns `401` when a request has neither an authenticated user nor a bot resource, instead of continuing. This closes the same gap for any controller that relies on the helper. - The dashboard direct-upload paths (`useFileUpload.js` and the legacy `fileUploadMixin.js`) now attach the agent's session headers to the upload request via a new `directUploadsHelper.js`, instead of sending `currentUser.access_token`. ## How to test 1. As a logged-in agent, open a conversation and attach a file. Upload should succeed as before, on installs with direct uploads enabled. 2. Confirm attachments still work for an agent on an account whose profile access token is not serialized (e.g. a Cloud plan without `api_and_webhooks`). 3. Send a `POST` to `/api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads` with no credentials, an empty `api_access_token`, or an invalid token, and confirm it returns `401`. 4. Confirm a valid agent of the account (via API token or session) gets `200`, while an agent of a different account gets `401`.
This commit is contained in:
@@ -7,27 +7,120 @@ RSpec.describe '/api/v1/accounts/:account_id/conversations/:conversation_id/dire
|
||||
let(:contact) { create(:contact, account: account, email: nil) }
|
||||
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
|
||||
let(:conversation) { create(:conversation, contact: contact, account: account, inbox: web_widget.inbox, contact_inbox: contact_inbox) }
|
||||
let(:blob_params) do
|
||||
{
|
||||
blob: {
|
||||
filename: 'avatar.png',
|
||||
byte_size: '1234',
|
||||
checksum: 'dsjbsdhbfif3874823mnsdbf',
|
||||
content_type: 'image/png'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def create_direct_upload(headers)
|
||||
post api_v1_account_conversation_direct_uploads_path(account_id: account.id, conversation_id: conversation.display_id),
|
||||
params: blob_params,
|
||||
headers: headers,
|
||||
as: :json
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads' do
|
||||
context 'when post request is made' do
|
||||
it 'creates attachment message in conversation' do
|
||||
contact
|
||||
context 'when it is an unauthenticated request' do
|
||||
it 'returns unauthorized without any credentials' do
|
||||
create_direct_upload({})
|
||||
|
||||
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: agent.access_token.token },
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns unauthorized with an empty api_access_token header' do
|
||||
create_direct_upload({ api_access_token: '' })
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns unauthorized with an invalid api_access_token header' do
|
||||
create_direct_upload({ api_access_token: 'invalid-token' })
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated request with an api access token' do
|
||||
it 'creates the blob for the direct upload' do
|
||||
create_direct_upload({ api_access_token: agent.access_token.token })
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['content_type']).to eq('image/png')
|
||||
expect(response.parsed_body['content_type']).to eq('image/png')
|
||||
end
|
||||
|
||||
it 'returns unauthorized for an agent of another account' do
|
||||
other_agent = create(:user, account: create(:account), role: :agent)
|
||||
|
||||
create_direct_upload({ api_access_token: other_agent.access_token.token })
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'returns unauthorized for an agent bot token' do
|
||||
agent_bot = create(:agent_bot, account: account)
|
||||
|
||||
create_direct_upload({ api_access_token: agent_bot.access_token.token })
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account api_and_webhooks feature is disabled' do
|
||||
before do
|
||||
allow(Account).to receive(:find).and_call_original
|
||||
allow(Account).to receive(:find).with(account.id.to_s).and_return(account)
|
||||
allow(account).to receive(:api_and_webhooks_enabled?).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns forbidden for a token-authenticated request' do
|
||||
create_direct_upload({ api_access_token: agent.access_token.token })
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it 'still creates the blob for a session-authenticated request' do
|
||||
create_direct_upload(agent.create_new_auth_token)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['content_type']).to eq('image/png')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated session request' do
|
||||
it 'creates the blob for the direct upload' do
|
||||
create_direct_upload(agent.create_new_auth_token)
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['content_type']).to eq('image/png')
|
||||
end
|
||||
|
||||
it 'creates the blob when the serialized api access token is empty' do
|
||||
create_direct_upload(agent.create_new_auth_token.merge('api_access_token' => ''))
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['content_type']).to eq('image/png')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when forgery protection is enabled' do
|
||||
around do |example|
|
||||
original = ActionController::Base.allow_forgery_protection
|
||||
ActionController::Base.allow_forgery_protection = true
|
||||
example.run
|
||||
ActionController::Base.allow_forgery_protection = original
|
||||
end
|
||||
|
||||
it 'creates the blob for a token-authenticated request without a CSRF token' do
|
||||
create_direct_upload({ api_access_token: agent.access_token.token })
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['content_type']).to eq('image/png')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user