From 5bf6c0ddc3e4f35926063810902d222fd2c77518 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Wed, 22 Jul 2026 17:59:05 +0530 Subject: [PATCH] fix(security): enforce premium flags on SLA automation, Captain runtime, and v2-only scenarios --- .../api/v1/accounts/captain/scenarios_controller.rb | 8 ++++++++ enterprise/app/services/enterprise/action_service.rb | 2 ++ .../message_templates/hook_execution_service.rb | 9 ++++++++- .../api/v1/accounts/captain/scenarios_controller_spec.rb | 2 +- .../services/enterprise/action_service_spec.rb | 2 ++ .../message_templates/hook_execution_service_spec.rb | 1 + 6 files changed, 22 insertions(+), 2 deletions(-) diff --git a/enterprise/app/controllers/api/v1/accounts/captain/scenarios_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/scenarios_controller.rb index 923a6d838..5be1c9559 100644 --- a/enterprise/app/controllers/api/v1/accounts/captain/scenarios_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/captain/scenarios_controller.rb @@ -1,4 +1,5 @@ class Api::V1::Accounts::Captain::ScenariosController < Api::V1::Accounts::Captain::BaseController + before_action :ensure_captain_v2_enabled before_action -> { check_authorization(Captain::Scenario) } before_action :set_assistant before_action :set_scenario, only: [:show, :update, :destroy] @@ -24,6 +25,13 @@ class Api::V1::Accounts::Captain::ScenariosController < Api::V1::Accounts::Capta private + # Scenarios are a Captain v2-only feature; the shared base guard allows either flag, so require v2 here. + def ensure_captain_v2_enabled + return if current_account.feature_enabled?('captain_integration_v2') + + render json: { error: 'Captain is not enabled for this account' }, status: :forbidden + end + def set_assistant @assistant = account_assistants.find(params[:assistant_id]) end diff --git a/enterprise/app/services/enterprise/action_service.rb b/enterprise/app/services/enterprise/action_service.rb index c841f5054..623e0a472 100644 --- a/enterprise/app/services/enterprise/action_service.rb +++ b/enterprise/app/services/enterprise/action_service.rb @@ -1,6 +1,8 @@ module Enterprise::ActionService def add_sla(sla_policy_id) return if sla_policy_id.blank? + # SLA is a premium feature; automation rules must not keep applying SLAs once it is disabled. + return unless @account.feature_enabled?('sla') sla_policy = @account.sla_policies.find_by(id: sla_policy_id.first) return if sla_policy.nil? diff --git a/enterprise/app/services/enterprise/message_templates/hook_execution_service.rb b/enterprise/app/services/enterprise/message_templates/hook_execution_service.rb index 56dbc7245..bddac7581 100644 --- a/enterprise/app/services/enterprise/message_templates/hook_execution_service.rb +++ b/enterprise/app/services/enterprise/message_templates/hook_execution_service.rb @@ -50,7 +50,14 @@ module Enterprise::MessageTemplates::HookExecutionService end def should_process_captain_response? - conversation.pending? && message.incoming? && inbox.captain_assistant.present? + conversation.pending? && message.incoming? && inbox.captain_assistant.present? && captain_feature_enabled? + end + + # Captain is a premium feature; stop the runtime responder when it has been revoked, even if an + # assistant is still linked to the inbox. + def captain_feature_enabled? + account = conversation.account + account.feature_enabled?('captain_integration') || account.feature_enabled?('captain_integration_v2') end def perform_handoff diff --git a/spec/enterprise/controllers/api/v1/accounts/captain/scenarios_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/captain/scenarios_controller_spec.rb index 9d12739ca..232750d47 100644 --- a/spec/enterprise/controllers/api/v1/accounts/captain/scenarios_controller_spec.rb +++ b/spec/enterprise/controllers/api/v1/accounts/captain/scenarios_controller_spec.rb @@ -6,7 +6,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::Scenarios', type: :request do let(:agent) { create(:user, account: account, role: :agent) } let(:assistant) { create(:captain_assistant, account: account) } - before { account.enable_features!('captain_integration') } + before { account.enable_features!('captain_integration_v2') } def json_response JSON.parse(response.body, symbolize_names: true) diff --git a/spec/enterprise/services/enterprise/action_service_spec.rb b/spec/enterprise/services/enterprise/action_service_spec.rb index 9396dc15d..6fb165972 100644 --- a/spec/enterprise/services/enterprise/action_service_spec.rb +++ b/spec/enterprise/services/enterprise/action_service_spec.rb @@ -8,6 +8,8 @@ describe ActionService do let(:conversation) { create(:conversation, account: account) } let(:action_service) { described_class.new(conversation) } + before { account.enable_features!('sla') } + context 'when sla_policy_id is present' do it 'adds the sla policy to the conversation and create applied_sla entry' do action_service.add_sla([sla_policy.id]) diff --git a/spec/enterprise/services/enterprise/message_templates/hook_execution_service_spec.rb b/spec/enterprise/services/enterprise/message_templates/hook_execution_service_spec.rb index 9eacb0ba1..cdaccc606 100644 --- a/spec/enterprise/services/enterprise/message_templates/hook_execution_service_spec.rb +++ b/spec/enterprise/services/enterprise/message_templates/hook_execution_service_spec.rb @@ -8,6 +8,7 @@ RSpec.describe MessageTemplates::HookExecutionService do let(:assistant) { create(:captain_assistant, account: account) } before do + account.enable_features!('captain_integration') create(:captain_inbox, captain_assistant: assistant, inbox: inbox) end