From d4289fcb5e12358c9f3f5969031d6dd655f70fec Mon Sep 17 00:00:00 2001
From: Gavin Hinfey <ghinfey@gitlab.com>
Date: Fri, 16 Feb 2024 22:52:29 +0000
Subject: [PATCH] Implement BranchRules::UpdateService

This service handles changes to BranchRule objects.
The EE version returns errors if a custom branch rule is passed.

Changelog: changed
MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144630
---
 app/services/branch_rules/base_service.rb     | 11 ++-
 app/services/branch_rules/update_service.rb   | 45 +++++++++++
 .../protected_branches/update_service.rb      |  8 +-
 .../ee/branch_rules/update_service.rb         | 24 ++++++
 .../renaming_blocked_by_policy.rb             |  2 +-
 .../branch_rules/update_service_spec.rb       | 49 ++++++++++++
 .../branch_rules/update_service_spec.rb       | 77 +++++++++++++++++++
 .../protected_branches/update_service_spec.rb |  9 +++
 8 files changed, 220 insertions(+), 5 deletions(-)
 create mode 100644 app/services/branch_rules/update_service.rb
 create mode 100644 ee/app/services/ee/branch_rules/update_service.rb
 create mode 100644 ee/spec/services/branch_rules/update_service_spec.rb
 create mode 100644 spec/services/branch_rules/update_service_spec.rb

diff --git a/app/services/branch_rules/base_service.rb b/app/services/branch_rules/base_service.rb
index 17b3cbd472b61..5ed8ea3a23bec 100644
--- a/app/services/branch_rules/base_service.rb
+++ b/app/services/branch_rules/base_service.rb
@@ -4,12 +4,19 @@ module BranchRules
   class BaseService
     include Gitlab::Allowable
 
-    attr_accessor :project, :branch_rule, :current_user
+    attr_reader :project, :branch_rule, :current_user, :params
 
-    def initialize(branch_rule, user = nil)
+    def initialize(branch_rule, user = nil, params = {})
       @branch_rule = branch_rule
       @project = branch_rule.project
       @current_user = user
+      @params = params.slice(*permitted_params)
+    end
+
+    private
+
+    def permitted_params
+      []
     end
   end
 end
diff --git a/app/services/branch_rules/update_service.rb b/app/services/branch_rules/update_service.rb
new file mode 100644
index 0000000000000..6b79376663e91
--- /dev/null
+++ b/app/services/branch_rules/update_service.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module BranchRules
+  class UpdateService < BaseService
+    PERMITTED_PARAMS = %i[name].freeze
+
+    attr_reader :skip_authorization
+
+    def execute(skip_authorization: false)
+      @skip_authorization = skip_authorization
+
+      raise Gitlab::Access::AccessDeniedError unless can_update_branch_rule?
+
+      return update_protected_branch if branch_rule.instance_of?(Projects::BranchRule)
+
+      yield if block_given?
+
+      ServiceResponse.error(message: 'Unknown branch rule type.')
+    end
+
+    private
+
+    def permitted_params
+      PERMITTED_PARAMS
+    end
+
+    def can_update_branch_rule?
+      return true if skip_authorization
+
+      can?(current_user, :update_protected_branch, branch_rule)
+    end
+
+    def update_protected_branch
+      service = ProtectedBranches::UpdateService.new(project, current_user, params)
+
+      service_response = service.execute(branch_rule.protected_branch, skip_authorization: skip_authorization)
+
+      return ServiceResponse.success unless service_response.errors.any?
+
+      ServiceResponse.error(message: service_response.errors.full_messages)
+    end
+  end
+end
+
+BranchRules::UpdateService.prepend_mod
diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb
index 4b54bf929893f..0c44871f6dc92 100644
--- a/app/services/protected_branches/update_service.rb
+++ b/app/services/protected_branches/update_service.rb
@@ -2,8 +2,8 @@
 
 module ProtectedBranches
   class UpdateService < ProtectedBranches::BaseService
-    def execute(protected_branch)
-      raise Gitlab::Access::AccessDeniedError unless can?(current_user, :update_protected_branch, protected_branch)
+    def execute(protected_branch, skip_authorization: false)
+      raise Gitlab::Access::AccessDeniedError unless skip_authorization || authorized?(protected_branch)
 
       old_merge_access_levels = protected_branch.merge_access_levels.map(&:clone)
       old_push_access_levels = protected_branch.push_access_levels.map(&:clone)
@@ -16,6 +16,10 @@ def execute(protected_branch)
 
       protected_branch
     end
+
+    def authorized?(protected_branch)
+      can?(current_user, :update_protected_branch, protected_branch)
+    end
   end
 end
 
diff --git a/ee/app/services/ee/branch_rules/update_service.rb b/ee/app/services/ee/branch_rules/update_service.rb
new file mode 100644
index 0000000000000..61fdf9733e04a
--- /dev/null
+++ b/ee/app/services/ee/branch_rules/update_service.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module EE
+  module BranchRules
+    module UpdateService
+      extend ::Gitlab::Utils::Override
+
+      # rubocop:disable Cop/AvoidReturnFromBlocks -- see similar
+      # implementation and explanation in BranchRules::DestroyService
+      override :execute
+      def execute(skip_authorization: false)
+        super do
+          case branch_rule
+          when ::Projects::AllBranchesRule
+            return ServiceResponse.error(message: 'All branch rules cannot be updated')
+          when ::Projects::AllProtectedBranchesRule
+            return ServiceResponse.error(message: 'All protected branch rules cannot be updated')
+          end
+        end
+      end
+      # rubocop:enable Cop/AvoidReturnFromBlocks
+    end
+  end
+end
diff --git a/ee/app/services/ee/protected_branches/renaming_blocked_by_policy.rb b/ee/app/services/ee/protected_branches/renaming_blocked_by_policy.rb
index ad34fa7d10725..1debecfb01916 100644
--- a/ee/app/services/ee/protected_branches/renaming_blocked_by_policy.rb
+++ b/ee/app/services/ee/protected_branches/renaming_blocked_by_policy.rb
@@ -3,7 +3,7 @@
 module EE
   module ProtectedBranches
     module RenamingBlockedByPolicy
-      def execute(protected_branch)
+      def execute(protected_branch, skip_authorization: false)
         raise ::Gitlab::Access::AccessDeniedError if renaming? && blocked?(protected_branch)
 
         super
diff --git a/ee/spec/services/branch_rules/update_service_spec.rb b/ee/spec/services/branch_rules/update_service_spec.rb
new file mode 100644
index 0000000000000..395182b29e52e
--- /dev/null
+++ b/ee/spec/services/branch_rules/update_service_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BranchRules::UpdateService, feature_category: :source_code_management do
+  let_it_be(:project) { create(:project, :repository) }
+  let_it_be(:user) { create(:user) }
+  let_it_be(:name) { 'new_name' }
+  let_it_be(:params) { { name: name } }
+
+  describe '#execute' do
+    subject(:execute) { described_class.new(branch_rule, user, params).execute }
+
+    before do
+      allow(Ability).to receive(:allowed?).and_return(true)
+    end
+
+    context 'when branch_rule is a Projects::AllBranchesRule' do
+      let(:branch_rule) { Projects::AllBranchesRule.new(project) }
+
+      it 'returns an error response' do
+        response = execute
+        expect(response).to be_error
+        expect(response[:message]).to eq('All branch rules cannot be updated')
+      end
+    end
+
+    context 'when branch_rule is a Projects::AllProtectedBranchesRule' do
+      let(:branch_rule) { Projects::AllProtectedBranchesRule.new(project) }
+
+      it 'returns an error response' do
+        response = execute
+        expect(response).to be_error
+        expect(response[:message]).to eq('All protected branch rules cannot be updated')
+      end
+    end
+
+    context 'when branch_rule is a ProtectedBranch' do
+      let(:protected_branch) { create(:protected_branch) }
+      let(:branch_rule) { Projects::BranchRule.new(project, protected_branch) }
+
+      it 'returns a success response' do
+        response = execute
+        expect(response).to be_success
+        expect(protected_branch.reload.name).to eq(name)
+      end
+    end
+  end
+end
diff --git a/spec/services/branch_rules/update_service_spec.rb b/spec/services/branch_rules/update_service_spec.rb
new file mode 100644
index 0000000000000..a1cd12f2cb3c6
--- /dev/null
+++ b/spec/services/branch_rules/update_service_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BranchRules::UpdateService, feature_category: :source_code_management do
+  let_it_be(:project) { create(:project, :repository) }
+  let_it_be(:user) { create(:user) }
+  let_it_be(:protected_branch) { create(:protected_branch) }
+
+  describe '#execute' do
+    let(:branch_rule) { Projects::BranchRule.new(project, protected_branch) }
+    let(:action_allowed) { true }
+    let(:update_service) { ProtectedBranches::UpdateService }
+    let(:update_service_instance) { instance_double(update_service) }
+    let(:new_name) { 'new_name' }
+    let(:errors) { ["Error 1", "Error 2"] }
+    let(:params) { { name: new_name } }
+
+    subject(:execute) { described_class.new(branch_rule, user, params).execute }
+
+    before do
+      allow(Ability).to receive(:allowed?).and_return(true)
+      allow(Ability)
+        .to receive(:allowed?).with(user, :update_protected_branch, branch_rule)
+                              .and_return(action_allowed)
+    end
+
+    context 'when the current_user cannot update the branch rule' do
+      let(:action_allowed) { false }
+
+      it 'raises an access denied error' do
+        expect { execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+      end
+    end
+
+    context 'when branch_rule is a Projects::BranchRule' do
+      it 'updates the ProtectedBranch and returns a success execute' do
+        expect(execute[:status]).to eq(:success)
+        expect(protected_branch.reload.name).to eq(new_name)
+      end
+
+      context 'if the update fails' do
+        before do
+          allow(update_service).to receive(:new).and_return(update_service_instance)
+          allow(update_service_instance).to receive(:execute).and_return(protected_branch)
+          allow(protected_branch).to receive_message_chain(:errors, :any?).and_return(errors)
+          allow(protected_branch).to receive_message_chain(:errors, :full_messages).and_return(errors)
+        end
+
+        it 'returns an error' do
+          response = execute
+          expect(response[:message]).to eq(errors)
+          expect(response[:status]).to eq(:error)
+        end
+      end
+    end
+
+    context 'when branch_rule is a ProtectedBranch' do
+      let(:branch_rule) { protected_branch }
+
+      it 'returns an error' do
+        response = execute
+        expect(response[:message]).to eq('Unknown branch rule type.')
+        expect(response[:status]).to eq(:error)
+      end
+    end
+
+    context 'when unpermitted params are provided' do
+      let(:params) { { name: new_name, not_permitted: 'not_permitted' } }
+
+      it 'removes them' do
+        expect(update_service).to receive(:new).with(project, user, { name: new_name }).and_call_original
+        execute
+      end
+    end
+  end
+end
diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb
index 8b11604aa15d7..73e721e015f81 100644
--- a/spec/services/protected_branches/update_service_spec.rb
+++ b/spec/services/protected_branches/update_service_spec.rb
@@ -40,6 +40,15 @@
           expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
         end
       end
+
+      context 'with skip authorization and unauthorized user' do
+        let(:user) { create(:user) }
+        let(:result) { service.execute(protected_branch, skip_authorization: true) }
+
+        it 'updates a protected branch' do
+          expect(result.reload.name).to eq(params[:name])
+        end
+      end
     end
   end
 
-- 
GitLab