From ac55f7a651028e9c2bafef90aad6180d6a6179b7 Mon Sep 17 00:00:00 2001
From: Hitesh Raghuvanshi <hraghuvanshi@gitlab.com>
Date: Mon, 12 Jun 2023 21:49:55 +0530
Subject: [PATCH 1/2] Update API for instance headers

Changelog: added
EE: true

GraphQL API for updating streaming headers in instance level audit
event external destinations
---
 doc/api/graphql/reference/index.md            |  27 ++++
 ee/app/graphql/ee/types/mutation_type.rb      |   1 +
 .../streaming/instance_headers/base.rb        |  10 ++
 .../streaming/instance_headers/update.rb      |  45 ++++++
 .../streaming/headers/update_service.rb       |  10 +-
 .../streaming/headers_operations.rb           |   8 +
 .../instance_headers/update_service.rb        |  14 ++
 .../streaming/instance_headers/update_spec.rb | 143 ++++++++++++++++++
 .../streaming/headers/update_service_spec.rb  |   8 +-
 .../instance_headers/update_service_spec.rb   |  27 ++++
 .../headers_operations_shared_examples.rb     |  37 +++++
 11 files changed, 318 insertions(+), 12 deletions(-)
 create mode 100644 ee/app/graphql/mutations/audit_events/streaming/instance_headers/update.rb
 create mode 100644 ee/app/services/audit_events/streaming/instance_headers/update_service.rb
 create mode 100644 ee/spec/requests/api/graphql/audit_events/streaming/instance_headers/update_spec.rb
 create mode 100644 ee/spec/services/audit_events/streaming/instance_headers/update_service_spec.rb

diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index ff5eeafaadf826..9066d3fe585bf0 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1293,6 +1293,27 @@ Input type: `AuditEventsStreamingInstanceHeadersCreateInput`
 | <a id="mutationauditeventsstreaminginstanceheaderscreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
 | <a id="mutationauditeventsstreaminginstanceheaderscreateheader"></a>`header` | [`AuditEventsStreamingInstanceHeader`](#auditeventsstreaminginstanceheader) | Created header. |
 
+### `Mutation.auditEventsStreamingInstanceHeadersUpdate`
+
+Input type: `AuditEventsStreamingInstanceHeadersUpdateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationauditeventsstreaminginstanceheadersupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationauditeventsstreaminginstanceheadersupdateheaderid"></a>`headerId` | [`AuditEventsStreamingInstanceHeaderID!`](#auditeventsstreaminginstanceheaderid) | Header to update. |
+| <a id="mutationauditeventsstreaminginstanceheadersupdatekey"></a>`key` | [`String!`](#string) | Header key. |
+| <a id="mutationauditeventsstreaminginstanceheadersupdatevalue"></a>`value` | [`String!`](#string) | Header value. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationauditeventsstreaminginstanceheadersupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationauditeventsstreaminginstanceheadersupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationauditeventsstreaminginstanceheadersupdateheader"></a>`header` | [`AuditEventsStreamingInstanceHeader`](#auditeventsstreaminginstanceheader) | Updates header. |
+
 ### `Mutation.awardEmojiAdd`
 
 Input type: `AwardEmojiAddInput`
@@ -26746,6 +26767,12 @@ A `AuditEventsStreamingHeaderID` is a global ID. It is encoded as a string.
 
 An example `AuditEventsStreamingHeaderID` is: `"gid://gitlab/AuditEvents::Streaming::Header/1"`.
 
+### `AuditEventsStreamingInstanceHeaderID`
+
+A `AuditEventsStreamingInstanceHeaderID` is a global ID. It is encoded as a string.
+
+An example `AuditEventsStreamingInstanceHeaderID` is: `"gid://gitlab/AuditEvents::Streaming::InstanceHeader/1"`.
+
 ### `AwardableID`
 
 A `AwardableID` is a global ID. It is encoded as a string.
diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb
index 56a4bf5aeda511..1cfef0d8ecf8b8 100644
--- a/ee/app/graphql/ee/types/mutation_type.rb
+++ b/ee/app/graphql/ee/types/mutation_type.rb
@@ -120,6 +120,7 @@ module MutationType
         mount_mutation ::Mutations::AuditEvents::GoogleCloudLoggingConfigurations::Update
         mount_mutation ::Mutations::Forecasting::BuildForecast, alpha: { milestone: '16.0' }
         mount_mutation ::Mutations::AuditEvents::Streaming::InstanceHeaders::Create
+        mount_mutation ::Mutations::AuditEvents::Streaming::InstanceHeaders::Update
 
         prepend(Types::DeprecatedMutations)
       end
diff --git a/ee/app/graphql/mutations/audit_events/streaming/instance_headers/base.rb b/ee/app/graphql/mutations/audit_events/streaming/instance_headers/base.rb
index 126de195516899..b2d0fbd78797a4 100644
--- a/ee/app/graphql/mutations/audit_events/streaming/instance_headers/base.rb
+++ b/ee/app/graphql/mutations/audit_events/streaming/instance_headers/base.rb
@@ -29,6 +29,16 @@ def find_destination(destination_id)
 
             destination
           end
+
+          def find_header(header_id)
+            header = GitlabSchema.object_from_id(
+              header_id, expected_type: ::AuditEvents::Streaming::InstanceHeader
+            ).sync
+
+            raise_resource_not_available_error! if header.blank?
+
+            header
+          end
         end
       end
     end
diff --git a/ee/app/graphql/mutations/audit_events/streaming/instance_headers/update.rb b/ee/app/graphql/mutations/audit_events/streaming/instance_headers/update.rb
new file mode 100644
index 00000000000000..2081f0c37ff821
--- /dev/null
+++ b/ee/app/graphql/mutations/audit_events/streaming/instance_headers/update.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Mutations
+  module AuditEvents
+    module Streaming
+      module InstanceHeaders
+        class Update < Base
+          graphql_name 'AuditEventsStreamingInstanceHeadersUpdate'
+
+          argument :header_id, ::Types::GlobalIDType[::AuditEvents::Streaming::InstanceHeader],
+            required: true,
+            description: 'Header to update.'
+
+          argument :key, GraphQL::Types::String,
+            required: true,
+            description: 'Header key.'
+
+          argument :value, GraphQL::Types::String,
+            required: true,
+            description: 'Header value.'
+
+          field :header, ::Types::AuditEvents::Streaming::InstanceHeaderType,
+            null: true,
+            description: 'Updates header.'
+
+          def resolve(header_id:, key:, value:)
+            header = find_header(header_id)
+
+            response = ::AuditEvents::Streaming::InstanceHeaders::UpdateService.new(
+              params: { header: header, key: key, value: value }
+            ).execute
+
+            if response.success?
+              { header: response.payload[:header], errors: [] }
+            elsif header.present?
+              { header: header.reset, errors: response.errors }
+            else
+              { errors: response.errors }
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/services/audit_events/streaming/headers/update_service.rb b/ee/app/services/audit_events/streaming/headers/update_service.rb
index c8bb539c5021a5..b6bba694ef9366 100644
--- a/ee/app/services/audit_events/streaming/headers/update_service.rb
+++ b/ee/app/services/audit_events/streaming/headers/update_service.rb
@@ -11,12 +11,10 @@ def execute
 
           audit_message = audit_message(header.key, params[:key])
 
-          if header.update(key: params[:key], value: params[:value])
-            audit(action: :update, header: header, message: audit_message)
-            ServiceResponse.success(payload: { header: header, errors: [] })
-          else
-            ServiceResponse.error(message: Array(header.errors))
-          end
+          success, response = update_header(header, params[:key], params[:value])
+
+          audit(action: :update, header: header, message: audit_message) if success
+          response
         end
 
         private
diff --git a/ee/app/services/audit_events/streaming/headers_operations.rb b/ee/app/services/audit_events/streaming/headers_operations.rb
index 1da4c4b612c324..fd4f3f787990aa 100644
--- a/ee/app/services/audit_events/streaming/headers_operations.rb
+++ b/ee/app/services/audit_events/streaming/headers_operations.rb
@@ -10,6 +10,14 @@ def create_header(destination, key, value)
 
         [false, ServiceResponse.error(message: Array(header.errors)), nil]
       end
+
+      def update_header(header, key, value)
+        if header.update(key: key, value: value)
+          [true, ServiceResponse.success(payload: { header: header, errors: [] })]
+        else
+          [false, ServiceResponse.error(message: Array(header.errors))]
+        end
+      end
     end
   end
 end
diff --git a/ee/app/services/audit_events/streaming/instance_headers/update_service.rb b/ee/app/services/audit_events/streaming/instance_headers/update_service.rb
new file mode 100644
index 00000000000000..d21cbc1fd5c75e
--- /dev/null
+++ b/ee/app/services/audit_events/streaming/instance_headers/update_service.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module AuditEvents
+  module Streaming
+    module InstanceHeaders
+      class UpdateService < BaseService
+        def execute
+          _, response = update_header(params[:header], params[:key], params[:value])
+          response
+        end
+      end
+    end
+  end
+end
diff --git a/ee/spec/requests/api/graphql/audit_events/streaming/instance_headers/update_spec.rb b/ee/spec/requests/api/graphql/audit_events/streaming/instance_headers/update_spec.rb
new file mode 100644
index 00000000000000..156971fc27f91f
--- /dev/null
+++ b/ee/spec/requests/api/graphql/audit_events/streaming/instance_headers/update_spec.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Update an external audit event destination header', feature_category: :audit_events do
+  include GraphqlHelpers
+
+  let_it_be(:destination) { create(:instance_external_audit_event_destination) }
+  let_it_be(:header) do
+    create(:instance_audit_events_streaming_header,
+      key: 'key-1',
+      instance_external_audit_event_destination: destination
+    )
+  end
+
+  let_it_be(:admin) { create(:admin) }
+  let_it_be(:user) { create(:user) }
+
+  let(:current_user) { admin }
+  let(:mutation) { graphql_mutation(:audit_events_streaming_instance_headers_update, input) }
+  let(:mutation_response) { graphql_mutation_response(:audit_events_streaming_instance_headers_update) }
+
+  let(:input) do
+    {
+      headerId: header.to_gid,
+      key: 'new-key',
+      value: 'new-value'
+    }
+  end
+
+  subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+  shared_examples 'a mutation that does not update a header' do
+    it 'does not update a header key' do
+      expect { post_graphql_mutation(mutation, current_user: actioner) }.not_to change { header.key }
+    end
+
+    it 'does not update a header value' do
+      expect { post_graphql_mutation(mutation, current_user: actioner) }.not_to change { header.value }
+    end
+  end
+
+  context 'when feature is licensed' do
+    before do
+      stub_licensed_features(external_audit_events: true)
+    end
+
+    context 'when feature flag `ff_external_audit_events` is enabled' do
+      context 'when current user is instance admin' do
+        it 'updates the header with the correct attributes', :aggregate_failures do
+          expect { subject }.to change { header.reload.key }.from('key-1').to('new-key')
+                                                            .and change { header.reload.value }.from('bar')
+                                                                                               .to('new-value')
+        end
+
+        context 'when the header attributes are invalid' do
+          let(:invalid_key_input) do
+            {
+              headerId: header.to_gid,
+              key: '',
+              value: 'bar'
+            }
+          end
+
+          let(:mutation) { graphql_mutation(:audit_events_streaming_instance_headers_update, invalid_key_input) }
+
+          it 'returns correct errors' do
+            subject
+
+            expect(mutation_response['errors']).to contain_exactly("Key can't be blank")
+          end
+
+          it 'returns the unmutated attribute values', :aggregate_failures do
+            subject
+
+            expect(mutation_response.dig('header', 'key')).to eq('key-1')
+            expect(mutation_response.dig('header', 'value')).to eq('bar')
+          end
+
+          it_behaves_like 'a mutation that does not update a header' do
+            let_it_be(:actioner) { admin }
+          end
+        end
+
+        context 'when the header id is wrong' do
+          let_it_be(:invalid_header_input) do
+            {
+              headerId: "gid://gitlab/AuditEvents::Streaming::InstanceHeader/-1",
+              key: 'foo',
+              value: 'bar'
+            }
+          end
+
+          let(:mutation) { graphql_mutation(:audit_events_streaming_instance_headers_update, invalid_header_input) }
+
+          it_behaves_like 'a mutation that returns top-level errors',
+            errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+
+          it_behaves_like 'a mutation that does not update a header' do
+            let_it_be(:actioner) { admin }
+          end
+        end
+      end
+
+      context 'when current user is not instance admin' do
+        it_behaves_like 'a mutation that does not update a header' do
+          let_it_be(:actioner) { user }
+        end
+      end
+    end
+
+    context 'when feature flag `ff_external_audit_events` is disabled' do
+      before do
+        stub_feature_flags(ff_external_audit_events: false)
+      end
+
+      context 'when current user is instance admin' do
+        it_behaves_like 'a mutation that does not update a header' do
+          let_it_be(:actioner) { admin }
+        end
+      end
+
+      context 'when current user is not instance admin' do
+        it_behaves_like 'a mutation that does not update a header' do
+          let_it_be(:actioner) { user }
+        end
+      end
+    end
+  end
+
+  context 'when feature is unlicensed' do
+    before do
+      stub_licensed_features(external_audit_events: false)
+    end
+
+    it_behaves_like 'a mutation that returns top-level errors',
+      errors: [Mutations::AuditEvents::Streaming::InstanceHeaders::Base::ERROR_MESSAGE]
+
+    it_behaves_like 'a mutation that does not update a header' do
+      let_it_be(:actioner) { admin }
+    end
+  end
+end
diff --git a/ee/spec/services/audit_events/streaming/headers/update_service_spec.rb b/ee/spec/services/audit_events/streaming/headers/update_service_spec.rb
index f40e312317caf5..5e54087578976d 100644
--- a/ee/spec/services/audit_events/streaming/headers/update_service_spec.rb
+++ b/ee/spec/services/audit_events/streaming/headers/update_service_spec.rb
@@ -41,13 +41,9 @@
       end
     end
 
-    context 'when the header is updated successfully' do
-      it 'updates the header' do
-        expect(response).to be_success
-        expect(header.reload.key).to eq 'new'
-        expect(header.value).to eq 'new'
-      end
+    it_behaves_like 'header updation'
 
+    context 'when the header is updated successfully' do
       it 'sends the audit streaming event' do
         audit_context = {
           name: 'audit_events_streaming_headers_update',
diff --git a/ee/spec/services/audit_events/streaming/instance_headers/update_service_spec.rb b/ee/spec/services/audit_events/streaming/instance_headers/update_service_spec.rb
new file mode 100644
index 00000000000000..bee161718b92f6
--- /dev/null
+++ b/ee/spec/services/audit_events/streaming/instance_headers/update_service_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AuditEvents::Streaming::InstanceHeaders::UpdateService, feature_category: :audit_events do
+  let(:header) { create(:instance_audit_events_streaming_header, key: 'old', value: 'old') }
+
+  let(:params) do
+    {
+      header: header,
+      key: 'new',
+      value: 'new'
+    }
+  end
+
+  subject(:service) do
+    described_class.new(
+      params: params
+    )
+  end
+
+  describe '#execute' do
+    subject(:response) { service.execute }
+
+    it_behaves_like 'header updation'
+  end
+end
diff --git a/ee/spec/support/shared_examples/services/audit_events/streaming/headers_operations_shared_examples.rb b/ee/spec/support/shared_examples/services/audit_events/streaming/headers_operations_shared_examples.rb
index d7fe64df8b04be..b024535e2bf4a4 100644
--- a/ee/spec/support/shared_examples/services/audit_events/streaming/headers_operations_shared_examples.rb
+++ b/ee/spec/support/shared_examples/services/audit_events/streaming/headers_operations_shared_examples.rb
@@ -27,3 +27,40 @@
     expect(header.value).to eq('a_value')
   end
 end
+
+RSpec.shared_examples 'header updation' do
+  context 'when header updation is successful' do
+    it 'has the header in the response payload' do
+      expect(response).to be_success
+      expect(response.payload[:header].key).to eq 'new'
+      expect(response.payload[:header].value).to eq 'new'
+    end
+
+    it 'updates the header' do
+      expect(response).to be_success
+      expect(header.reload.key).to eq 'new'
+      expect(header.value).to eq 'new'
+    end
+  end
+
+  context 'when header updation is unsuccessful' do
+    let(:params) do
+      {
+        header: header,
+        key: '',
+        value: 'new'
+      }
+    end
+
+    it 'does not update the header' do
+      expect { subject }.not_to change { header.reload.key }
+      expect(header.value).to eq 'old'
+    end
+
+    it 'has an error response' do
+      expect(response).to be_error
+      expect(response.errors)
+        .to match_array ["Key can't be blank"]
+    end
+  end
+end
-- 
GitLab


From 27f3964195e980d67c054a11e5c5f089666a22cc Mon Sep 17 00:00:00 2001
From: Hitesh Raghuvanshi <hraghuvanshi@gitlab.com>
Date: Tue, 13 Jun 2023 15:35:37 +0530
Subject: [PATCH 2/2] Removed unnecessary condition

---
 .../mutations/audit_events/streaming/instance_headers/update.rb | 2 --
 1 file changed, 2 deletions(-)

diff --git a/ee/app/graphql/mutations/audit_events/streaming/instance_headers/update.rb b/ee/app/graphql/mutations/audit_events/streaming/instance_headers/update.rb
index 2081f0c37ff821..008c2365737c22 100644
--- a/ee/app/graphql/mutations/audit_events/streaming/instance_headers/update.rb
+++ b/ee/app/graphql/mutations/audit_events/streaming/instance_headers/update.rb
@@ -34,8 +34,6 @@ def resolve(header_id:, key:, value:)
               { header: response.payload[:header], errors: [] }
             elsif header.present?
               { header: header.reset, errors: response.errors }
-            else
-              { errors: response.errors }
             end
           end
         end
-- 
GitLab