fix: incorrect scope across controllers (#14459)

Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
Shivam Mishra
2026-05-14 20:34:18 +05:30
committed by GitHub
co-authored by Sojan Jose
parent fbcb89e955
commit 13f66e3a88
17 changed files with 173 additions and 46 deletions
@@ -87,7 +87,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def fetch_agent_bot
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
@agent_bot = AgentBot.accessible_to(Current.account).find(params[:agent_bot]) if params[:agent_bot]
end
def create_channel
@@ -41,9 +41,9 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
def destroy_all
if params[:type] == 'read'
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :read)
::Notification::DeleteNotificationJob.perform_later(Current.user, Current.account, type: :read)
else
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :all)
::Notification::DeleteNotificationJob.perform_later(Current.user, Current.account, type: :all)
end
head :ok
end
@@ -69,7 +69,7 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
end
def fetch_notification
@notification = current_user.notifications.find(params[:id])
@notification = current_user.notifications.where(account_id: Current.account.id).find(params[:id])
end
def set_current_page
@@ -87,7 +87,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
return {} unless permitted_params.key?(:inbox_id)
return { channel_web_widget_id: nil } if permitted_params[:inbox_id].blank?
inbox = Inbox.find(permitted_params[:inbox_id])
inbox = Current.account.inboxes.find(permitted_params[:inbox_id])
return {} unless inbox.web_widget?
{ channel_web_widget_id: inbox.channel.id }
@@ -8,7 +8,8 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
end
def destroy
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
notification_subscription = current_user.notification_subscriptions
.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
notification_subscription.destroy! if notification_subscription.present?
head :ok
end
@@ -83,6 +83,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
end
def set_message
@message = @web_widget.inbox.messages.find(permitted_params[:id])
# `conversation.messages.find` would be simpler, but `conversation` is `conversations.last`,
# which means a visitor with more than one open thread could not edit a message in any
# but their most recent one. Scoping across all of the visitor's conversations keeps the
# happy path correct for that future multi-conversation widget flow.
@message = Message.where(conversation_id: conversations.select(:id)).find(permitted_params[:id])
end
end
@@ -24,6 +24,10 @@ class Public::Api::V1::InboxesController < PublicController
def set_conversation
return if params[:conversation_id].blank?
@conversation = @contact_inbox.contact.conversations.find_by!(display_id: params[:conversation_id])
@conversation = if @contact_inbox.hmac_verified?
@contact_inbox.contact.conversations.find_by!(display_id: params[:conversation_id])
else
@contact_inbox.conversations.find_by!(display_id: params[:conversation_id])
end
end
end
+3 -1
View File
@@ -8,6 +8,7 @@ class BulkActionsJob < ApplicationJob
def perform(account:, params:, user:)
@account = account
@user = user
Current.user = user
@params = params
@records = records_to_updated(params[:ids])
@@ -61,6 +62,7 @@ class BulkActionsJob < ApplicationJob
current_model = @params[:type].camelcase
return unless MODEL_TYPE.include?(current_model)
current_model.constantize&.where(account_id: @account.id, display_id: ids)
scope = current_model.constantize.where(account_id: @account.id, display_id: ids)
Conversations::PermissionFilterService.new(scope, @user, @account).perform
end
end
@@ -1,14 +1,14 @@
class Notification::DeleteNotificationJob < ApplicationJob
queue_as :low
def perform(user, type: :all)
def perform(user, account, type: :all)
notifications = user.notifications.where(account_id: account.id)
ActiveRecord::Base.transaction do
if type == :all
# Delete all notifications
user.notifications.destroy_all
notifications.destroy_all
elsif type == :read
# Delete only read notifications
user.notifications.where.not(read_at: nil).destroy_all
notifications.where.not(read_at: nil).destroy_all
end
end
end
@@ -34,6 +34,10 @@ RSpec.describe 'Api::V1::Accounts::BulkActionsController', type: :request do
context 'when it is an authenticated user' do
let!(:agent) { create(:user, account: account, role: :agent) }
before do
Conversation.all.find_each { |conversation| create(:inbox_member, inbox: conversation.inbox, user: agent) }
end
it 'Ignores bulk_actions for wrong type' do
post "/api/v1/accounts/#{account.id}/bulk_actions",
headers: agent.create_new_auth_token,
@@ -202,6 +206,10 @@ RSpec.describe 'Api::V1::Accounts::BulkActionsController', type: :request do
context 'when it is an authenticated user' do
let!(:agent) { create(:user, account: account, role: :agent) }
before do
Conversation.all.find_each { |conversation| create(:inbox_member, inbox: conversation.inbox, user: agent) }
end
it 'Bulk delete conversation labels' do
Conversation.first.add_labels(%w[support priority_customer])
Conversation.second.add_labels(%w[support priority_customer])
@@ -1008,6 +1008,19 @@ RSpec.describe 'Inboxes API', type: :request do
expect(response).to have_http_status(:unauthorized)
end
it 'does not allow binding an agent bot from another account' do
other_account = create(:account)
foreign_bot = create(:agent_bot, account: other_account)
post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/set_agent_bot",
headers: admin.create_new_auth_token,
params: { agent_bot: foreign_bot.id },
as: :json
expect(response).to have_http_status(:not_found)
expect(inbox.reload.agent_bot).to be_nil
end
end
end
@@ -101,6 +101,20 @@ RSpec.describe 'Notifications API', type: :request do
expect(response).to have_http_status(:success)
expect(notification.reload.read_at).not_to eq('')
end
it 'does not update a notification reached via a different account that the user belongs to' do
other_account = create(:account)
create(:account_user, account: other_account, user: admin, role: :administrator)
original_read_at = notification.read_at
patch "/api/v1/accounts/#{other_account.id}/notifications/#{notification.id}",
headers: admin.create_new_auth_token,
params: { read_at: true },
as: :json
expect(response).to have_http_status(:not_found)
expect(notification.reload.read_at).to eq(original_read_at)
end
end
end
@@ -227,7 +241,7 @@ RSpec.describe 'Notifications API', type: :request do
let(:admin) { create(:user, account: account, role: :administrator) }
it 'deletes all the read notifications' do
expect(Notification::DeleteNotificationJob).to receive(:perform_later).with(admin, type: :read)
expect(Notification::DeleteNotificationJob).to receive(:perform_later).with(admin, account, type: :read)
post "/api/v1/accounts/#{account.id}/notifications/destroy_all",
headers: admin.create_new_auth_token,
@@ -238,7 +252,7 @@ RSpec.describe 'Notifications API', type: :request do
end
it 'deletes all the notifications' do
expect(Notification::DeleteNotificationJob).to receive(:perform_later).with(admin, type: :all)
expect(Notification::DeleteNotificationJob).to receive(:perform_later).with(admin, account, type: :all)
post "/api/v1/accounts/#{account.id}/notifications/destroy_all",
headers: admin.create_new_auth_token,
@@ -192,6 +192,21 @@ RSpec.describe 'Api::V1::Accounts::Portals', type: :request do
expect(portal.reload.logo).to be_attached
end
it 'does not allow associating an inbox from another account' do
other_account = create(:account)
foreign_inbox = create(:inbox, account: other_account)
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
params: {
portal: { name: portal.name },
inbox_id: foreign_inbox.id
},
headers: admin.create_new_auth_token
expect(response).to have_http_status(:not_found)
expect(portal.reload.channel_web_widget_id).to be_nil
end
it 'clears associated web widget when inbox selection is blank' do
web_widget_inbox = create(:inbox, account: account)
portal.update!(channel_web_widget: web_widget_inbox.channel)
@@ -106,6 +106,21 @@ RSpec.describe 'Notifications Subscriptions API', type: :request do
expect(response).to have_http_status(:success)
expect { subscription.reload }.to raise_exception(ActiveRecord::RecordNotFound)
end
it 'does not delete another user notification subscription with the same push token' do
victim = create(:user, account: account, role: :agent)
victim_subscription = create(:notification_subscription, subscription_type: 'fcm',
subscription_attributes: { push_token: 'victimToken' },
user: victim)
delete '/api/v1/notification_subscriptions',
params: { push_token: 'victimToken' },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect { victim_subscription.reload }.not_to raise_error
end
end
end
end
@@ -212,6 +212,26 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
end
describe 'PUT /api/v1/widget/messages' do
context 'when put request targets a message from another visitor in the same inbox' do
it 'does not update the foreign message' do
other_contact = create(:contact, account: account, email: nil)
other_contact_inbox = create(:contact_inbox, contact: other_contact, inbox: web_widget.inbox)
other_conversation = create(:conversation, contact: other_contact, account: account,
inbox: web_widget.inbox, contact_inbox: other_contact_inbox)
foreign_message = create(:message, content_type: 'input_email', account: account,
inbox: web_widget.inbox, conversation: other_conversation)
original_email = foreign_message.submitted_email
put api_v1_widget_message_url(foreign_message.id),
params: { website_token: web_widget.website_token, contact: { email: Faker::Internet.email } },
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:not_found)
expect(foreign_message.reload.submitted_email).to eq(original_email)
end
end
context 'when put request is made with non existing email' do
it 'updates message in conversation and creates a new contact' do
message = create(:message, content_type: 'input_email', account: account, inbox: web_widget.inbox, conversation: conversation)
@@ -16,6 +16,19 @@ RSpec.describe 'Public Inbox Contact Conversation Messages API', type: :request
data = response.parsed_body
expect(data.length).to eq 2
end
it 'does not return messages from a conversation in another inbox even when both share the same contact' do
other_channel = create(:channel_api, account: conversation.account)
other_contact_inbox = create(:contact_inbox, contact: contact, inbox: other_channel.inbox)
foreign_conversation = create(:conversation, contact: contact, account: conversation.account,
inbox: other_channel.inbox, contact_inbox: other_contact_inbox)
create(:message, account: foreign_conversation.account, inbox: foreign_conversation.inbox, conversation: foreign_conversation)
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/" \
"#{foreign_conversation.display_id}/messages"
expect(response).to have_http_status(:not_found)
end
end
describe 'POST /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations/{conversation_id}/messages' do
+29 -19
View File
@@ -1,12 +1,6 @@
require 'rails_helper'
RSpec.describe BulkActionsJob do
params = {
type: 'Conversation',
fields: { status: 'snoozed' },
ids: Conversation.first(3).pluck(:display_id)
}
subject(:job) { described_class.perform_later(account: account, params: params, user: agent) }
let(:account) { create(:account) }
@@ -14,9 +8,11 @@ RSpec.describe BulkActionsJob do
let!(:conversation_1) { create(:conversation, account_id: account.id, status: :open) }
let!(:conversation_2) { create(:conversation, account_id: account.id, status: :open) }
let!(:conversation_3) { create(:conversation, account_id: account.id, status: :open) }
let(:conversation_ids) { [conversation_1.display_id, conversation_2.display_id, conversation_3.display_id] }
let(:params) { { type: 'Conversation', fields: { status: 'snoozed' }, ids: conversation_ids } }
before do
Conversation.all.find_each do |conversation|
[conversation_1, conversation_2, conversation_3].each do |conversation|
create(:inbox_member, inbox: conversation.inbox, user: agent)
end
end
@@ -38,10 +34,10 @@ RSpec.describe BulkActionsJob do
params = {
type: 'Conversation',
fields: { status: 'snoozed', assignee_id: agent.id },
ids: Conversation.first(3).pluck(:display_id)
ids: conversation_ids
}
expect(Conversation.first.status).to eq('open')
expect(conversation_1.status).to eq('open')
described_class.perform_now(account: account, params: params, user: agent)
@@ -54,32 +50,46 @@ RSpec.describe BulkActionsJob do
params = {
type: 'Conversation',
fields: { status: 'snoozed', assignee_id: agent.id },
ids: Conversation.first(3).pluck(:display_id)
ids: conversation_ids
}
expect(Conversation.first.assignee_id).to be_nil
expect(conversation_1.assignee_id).to be_nil
described_class.perform_now(account: account, params: params, user: agent)
expect(Conversation.first.assignee_id).to eq(agent.id)
expect(Conversation.second.assignee_id).to eq(agent.id)
expect(Conversation.third.assignee_id).to eq(agent.id)
expect(conversation_1.reload.assignee_id).to eq(agent.id)
expect(conversation_2.reload.assignee_id).to eq(agent.id)
expect(conversation_3.reload.assignee_id).to eq(agent.id)
end
it 'bulk updates the snoozed_until' do
params = {
type: 'Conversation',
fields: { status: 'snoozed', snoozed_until: Time.zone.now },
ids: Conversation.first(3).pluck(:display_id)
ids: conversation_ids
}
expect(Conversation.first.snoozed_until).to be_nil
expect(conversation_1.snoozed_until).to be_nil
described_class.perform_now(account: account, params: params, user: agent)
expect(Conversation.first.snoozed_until).to be_present
expect(Conversation.second.snoozed_until).to be_present
expect(Conversation.third.snoozed_until).to be_present
expect(conversation_1.reload.snoozed_until).to be_present
expect(conversation_2.reload.snoozed_until).to be_present
expect(conversation_3.reload.snoozed_until).to be_present
end
it 'skips conversations whose inbox the agent does not belong to' do
forbidden_conversation = create(:conversation, account_id: account.id, status: :open)
params = {
type: 'Conversation',
fields: { status: 'resolved' },
ids: [conversation_1.display_id, forbidden_conversation.display_id]
}
described_class.perform_now(account: account, params: params, user: agent)
expect(conversation_1.reload.status).to eq('resolved')
expect(forbidden_conversation.reload.status).to eq('open')
end
end
end
@@ -2,37 +2,45 @@ require 'rails_helper'
RSpec.describe Notification::DeleteNotificationJob do
let(:user) { create(:user) }
let(:account) { create(:account) }
let(:conversation) { create(:conversation) }
context 'when enqueuing the job' do
it 'enqueues the job to delete all notifications' do
expect do
described_class.perform_later(user.id, type: :all)
described_class.perform_later(user, account, type: :all)
end.to have_enqueued_job(described_class).on_queue('low')
end
it 'enqueues the job to delete read notifications' do
expect do
described_class.perform_later(user.id, type: :read)
described_class.perform_later(user, account, type: :read)
end.to have_enqueued_job(described_class).on_queue('low')
end
end
context 'when performing the job' do
let(:other_account) { create(:account) }
before do
create(:notification, user: user, read_at: nil)
create(:notification, user: user, read_at: Time.current)
create(:notification, account: account, user: user, read_at: nil)
create(:notification, account: account, user: user, read_at: Time.current)
create(:notification, account: other_account, user: user, read_at: Time.current)
end
it 'deletes all notifications' do
described_class.perform_now(user, type: :all)
expect(user.notifications.count).to eq(0)
it 'deletes all notifications for the requested account' do
described_class.perform_now(user, account, type: :all)
expect(user.notifications.where(account_id: account.id).count).to eq(0)
expect(user.notifications.where(account_id: other_account.id).count).to eq(1)
end
it 'deletes only read notifications' do
described_class.perform_now(user, type: :read)
expect(user.notifications.count).to eq(1)
expect(user.notifications.where(read_at: nil).count).to eq(1)
it 'deletes only read notifications for the requested account' do
described_class.perform_now(user, account, type: :read)
expect(user.notifications.where(account_id: account.id).count).to eq(1)
expect(user.notifications.where(account_id: account.id, read_at: nil).count).to eq(1)
expect(user.notifications.where(account_id: other_account.id).count).to eq(1)
end
end
end