From 602c458ab6773e56e512c032c16fe4c7cddc1c44 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 22 Jun 2023 14:52:25 +0200
Subject: [PATCH] Add finer permission requirements for managing webhooks
 (#25463)

---
 app/controllers/admin/webhooks_controller.rb  |  3 +++
 app/models/webhook.rb                         | 22 +++++++++++++++++++
 app/policies/webhook_policy.rb                |  4 ++--
 app/views/admin/webhooks/_form.html.haml      |  2 +-
 config/locales/activerecord.en.yml            |  4 ++++
 .../admin/webhooks_controller_spec.rb         |  2 +-
 spec/policies/webhook_policy_spec.rb          | 22 ++++++++++++++++---
 7 files changed, 52 insertions(+), 7 deletions(-)

diff --git a/app/controllers/admin/webhooks_controller.rb b/app/controllers/admin/webhooks_controller.rb
index e08747665..f1aad7c4b 100644
--- a/app/controllers/admin/webhooks_controller.rb
+++ b/app/controllers/admin/webhooks_controller.rb
@@ -28,6 +28,7 @@ module Admin
       authorize :webhook, :create?
 
       @webhook = Webhook.new(resource_params)
+      @webhook.current_account = current_account
 
       if @webhook.save
         redirect_to admin_webhook_path(@webhook)
@@ -39,6 +40,8 @@ module Admin
     def update
       authorize @webhook, :update?
 
+      @webhook.current_account = current_account
+
       if @webhook.update(resource_params)
         redirect_to admin_webhook_path(@webhook)
       else
diff --git a/app/models/webhook.rb b/app/models/webhook.rb
index c46fce743..14f33c5fc 100644
--- a/app/models/webhook.rb
+++ b/app/models/webhook.rb
@@ -24,6 +24,8 @@ class Webhook < ApplicationRecord
     status.updated
   ).freeze
 
+  attr_writer :current_account
+
   scope :enabled, -> { where(enabled: true) }
 
   validates :url, presence: true, url: true
@@ -31,6 +33,7 @@ class Webhook < ApplicationRecord
   validates :events, presence: true
 
   validate :validate_events
+  validate :validate_permissions
   validate :validate_template
 
   before_validation :strip_events
@@ -48,12 +51,31 @@ class Webhook < ApplicationRecord
     update!(enabled: false)
   end
 
+  def required_permissions
+    events.map { |event| Webhook.permission_for_event(event) }
+  end
+
+  def self.permission_for_event(event)
+    case event
+    when 'account.approved', 'account.created', 'account.updated'
+      :manage_users
+    when 'report.created'
+      :manage_reports
+    when 'status.created', 'status.updated'
+      :view_devops
+    end
+  end
+
   private
 
   def validate_events
     errors.add(:events, :invalid) if events.any? { |e| EVENTS.exclude?(e) }
   end
 
+  def validate_permissions
+    errors.add(:events, :invalid_permissions) if defined?(@current_account) && required_permissions.any? { |permission| !@current_account.user_role.can?(permission) }
+  end
+
   def validate_template
     return if template.blank?
 
diff --git a/app/policies/webhook_policy.rb b/app/policies/webhook_policy.rb
index a2199a333..577e891b6 100644
--- a/app/policies/webhook_policy.rb
+++ b/app/policies/webhook_policy.rb
@@ -14,7 +14,7 @@ class WebhookPolicy < ApplicationPolicy
   end
 
   def update?
-    role.can?(:manage_webhooks)
+    role.can?(:manage_webhooks) && record.required_permissions.all? { |permission| role.can?(permission) }
   end
 
   def enable?
@@ -30,6 +30,6 @@ class WebhookPolicy < ApplicationPolicy
   end
 
   def destroy?
-    role.can?(:manage_webhooks)
+    role.can?(:manage_webhooks) && record.required_permissions.all? { |permission| role.can?(permission) }
   end
 end
diff --git a/app/views/admin/webhooks/_form.html.haml b/app/views/admin/webhooks/_form.html.haml
index 8d019ff43..c870e943f 100644
--- a/app/views/admin/webhooks/_form.html.haml
+++ b/app/views/admin/webhooks/_form.html.haml
@@ -5,7 +5,7 @@
     = f.input :url, wrapper: :with_block_label, input_html: { placeholder: 'https://' }
 
   .fields-group
-    = f.input :events, collection: Webhook::EVENTS, wrapper: :with_block_label, include_blank: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
+    = f.input :events, collection: Webhook::EVENTS, wrapper: :with_block_label, include_blank: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', disabled: Webhook::EVENTS.filter { |event| !current_user.role.can?(Webhook.permission_for_event(event)) }
 
   .fields-group
     = f.input :template, wrapper: :with_block_label, input_html: { placeholder: '{ "content": "Hello {{object.username}}" }' }
diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml
index 8aee15659..a53c7c6e9 100644
--- a/config/locales/activerecord.en.yml
+++ b/config/locales/activerecord.en.yml
@@ -53,3 +53,7 @@ en:
             position:
               elevated: cannot be higher than your current role
               own_role: cannot be changed with your current role
+        webhook:
+          attributes:
+            events:
+              invalid_permissions: cannot include events you don't have the rights to
diff --git a/spec/controllers/admin/webhooks_controller_spec.rb b/spec/controllers/admin/webhooks_controller_spec.rb
index 074956c55..0ccfbbcc6 100644
--- a/spec/controllers/admin/webhooks_controller_spec.rb
+++ b/spec/controllers/admin/webhooks_controller_spec.rb
@@ -48,7 +48,7 @@ describe Admin::WebhooksController do
   end
 
   context 'with an existing record' do
-    let!(:webhook) { Fabricate :webhook }
+    let!(:webhook) { Fabricate(:webhook, events: ['account.created', 'report.created']) }
 
     describe 'GET #show' do
       it 'returns http success and renders view' do
diff --git a/spec/policies/webhook_policy_spec.rb b/spec/policies/webhook_policy_spec.rb
index 1eac8932d..909311461 100644
--- a/spec/policies/webhook_policy_spec.rb
+++ b/spec/policies/webhook_policy_spec.rb
@@ -8,16 +8,32 @@ describe WebhookPolicy do
   let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
   let(:john)    { Fabricate(:account) }
 
-  permissions :index?, :create?, :show?, :update?, :enable?, :disable?, :rotate_secret?, :destroy? do
+  permissions :index?, :create? do
     context 'with an admin' do
       it 'permits' do
-        expect(policy).to permit(admin, Tag)
+        expect(policy).to permit(admin, Webhook)
       end
     end
 
     context 'with a non-admin' do
       it 'denies' do
-        expect(policy).to_not permit(john, Tag)
+        expect(policy).to_not permit(john, Webhook)
+      end
+    end
+  end
+
+  permissions :show?, :update?, :enable?, :disable?, :rotate_secret?, :destroy? do
+    let(:webhook) { Fabricate(:webhook, events: ['account.created', 'report.created']) }
+
+    context 'with an admin' do
+      it 'permits' do
+        expect(policy).to permit(admin, webhook)
+      end
+    end
+
+    context 'with a non-admin' do
+      it 'denies' do
+        expect(policy).to_not permit(john, webhook)
       end
     end
   end