From 49c442751d9c919bf72e65e745dec2955450f61c Mon Sep 17 00:00:00 2001 From: Gaurav Singhal Date: Tue, 14 Jul 2026 23:02:13 -0700 Subject: [PATCH] fix: require admin for dashboard app mutations (#14831) ## Summary Restricts account-wide Dashboard App creation, updates, and deletion to administrators while keeping read access available to authenticated account users. ## Why Dashboard Apps are account-level integrations displayed in conversation views. Agents should be able to use them, but only administrators should be able to change their configuration. ## What changed - authorize Dashboard App actions through `DashboardAppPolicy` - allow index and show access for authenticated account users - restrict create, update, and destroy actions to administrators - add request coverage for administrator and agent mutation behavior ## Validation `bundle exec rspec spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb` 17 examples, 0 failures. `bundle exec rubocop app/controllers/api/v1/accounts/dashboard_apps_controller.rb app/policies/dashboard_app_policy.rb` 2 files inspected, no offenses detected. --------- Co-authored-by: Gaurav Singhal Co-authored-by: Sojan Jose --- .../v1/accounts/dashboard_apps_controller.rb | 1 + app/policies/dashboard_app_policy.rb | 21 ++++++++ .../dashboard_apps_controller_spec.rb | 50 +++++++++++++++++-- 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 app/policies/dashboard_app_policy.rb diff --git a/app/controllers/api/v1/accounts/dashboard_apps_controller.rb b/app/controllers/api/v1/accounts/dashboard_apps_controller.rb index a8d7ebcb9..4226db1cc 100644 --- a/app/controllers/api/v1/accounts/dashboard_apps_controller.rb +++ b/app/controllers/api/v1/accounts/dashboard_apps_controller.rb @@ -1,4 +1,5 @@ class Api::V1::Accounts::DashboardAppsController < Api::V1::Accounts::BaseController + before_action :check_authorization before_action :fetch_dashboard_apps, except: [:create] before_action :fetch_dashboard_app, only: [:show, :update, :destroy] diff --git a/app/policies/dashboard_app_policy.rb b/app/policies/dashboard_app_policy.rb new file mode 100644 index 000000000..af7bec82a --- /dev/null +++ b/app/policies/dashboard_app_policy.rb @@ -0,0 +1,21 @@ +class DashboardAppPolicy < ApplicationPolicy + def index? + true + end + + def show? + true + end + + def create? + @account_user.administrator? + end + + def update? + @account_user.administrator? + end + + def destroy? + @account_user.administrator? + end +end diff --git a/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb b/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb index 100f914bb..820010a62 100644 --- a/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb @@ -70,8 +70,8 @@ RSpec.describe 'DashboardAppsController', type: :request do end end - context 'when it is an authenticated user' do - let(:user) { create(:user, account: account) } + context 'when it is an authenticated administrator' do + let(:user) { create(:user, account: account, role: :administrator) } it 'creates the dashboard app' do expect do @@ -130,11 +130,26 @@ RSpec.describe 'DashboardAppsController', type: :request do expect(response).to have_http_status(:unprocessable_entity) end end + + context 'when it is an authenticated agent' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'does not create account-wide dashboard apps' do + expect do + post "/api/v1/accounts/#{account.id}/dashboard_apps", + headers: agent.create_new_auth_token, + params: payload, + as: :json + end.not_to change(DashboardApp, :count) + + expect(response).to have_http_status(:unauthorized) + end + end end describe 'PATCH /api/v1/accounts/{account.id}/dashboard_apps/:id' do let(:payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'https://link.com' }] } } } - let(:user) { create(:user, account: account) } + let(:user) { create(:user, account: account, role: :administrator) } let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) } context 'when it is an unauthenticated user' do @@ -160,10 +175,24 @@ RSpec.describe 'DashboardAppsController', type: :request do expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type] end end + + context 'when it is an authenticated agent' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'does not update account-wide dashboard apps' do + patch "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}", + headers: agent.create_new_auth_token, + params: payload, + as: :json + + expect(response).to have_http_status(:unauthorized) + expect(dashboard_app.reload.title).not_to eq('CRM Dashboard') + end + end end describe 'DELETE /api/v1/accounts/{account.id}/dashboard_apps/:id' do - let(:user) { create(:user, account: account) } + let(:user) { create(:user, account: account, role: :administrator) } let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) } context 'when it is an unauthenticated user' do @@ -182,5 +211,18 @@ RSpec.describe 'DashboardAppsController', type: :request do expect(user.dashboard_apps.count).to be 0 end end + + context 'when it is an authenticated agent' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'does not delete account-wide dashboard apps' do + delete "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + expect(DashboardApp.exists?(dashboard_app.id)).to be(true) + end + end end end