diff --git a/config/audit_events/types/self_hosted_model_destroyed.yml b/config/audit_events/types/self_hosted_model_destroyed.yml new file mode 100644 index 0000000000000000000000000000000000000000..970227e234711d5f227551485429ac4b6b468865 --- /dev/null +++ b/config/audit_events/types/self_hosted_model_destroyed.yml @@ -0,0 +1,9 @@ +name: self_hosted_model_destroyed +description: A new self-hosted model configuration was destroyed +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/477999 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165321 +feature_category: self-hosted_models +milestone: '17.4' +saved_to_database: true +scope: [Instance, User] +streamed: true diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 2975ebb088a6415c7bd34c8eef44560ce3f83b21..7bb4011e38c1eb39f3ad37ebafddca3394f4735a 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -453,6 +453,7 @@ Audit event types belong to the following product categories. | Name | Description | Saved to database | Streamed | Introduced in | Scope | |:------------|:------------|:------------------|:---------|:--------------|:--------------| +| [`self_hosted_model_destroyed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165321) | A new self-hosted model configuration was destroyed | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.4](https://gitlab.com/gitlab-org/gitlab/-/issues/477999) | Instance, User | | [`self_hosted_model_feature_changed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165489) | A self-hosted model feature had its configuration changed | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.4](https://gitlab.com/gitlab-org/gitlab/-/issues/463215) | Project | | [`self_hosted_model_terms_accepted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165480) | Terms for usage of self-hosted models were accepted | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.4](https://gitlab.com/gitlab-org/gitlab/-/issues/477999) | Instance, User | diff --git a/ee/app/controllers/admin/ai/self_hosted_models_controller.rb b/ee/app/controllers/admin/ai/self_hosted_models_controller.rb index 7d8cead2550689473b249197af1915189a57bd9b..8e05afe3a9a19709c0804b378698da53400d4252 100644 --- a/ee/app/controllers/admin/ai/self_hosted_models_controller.rb +++ b/ee/app/controllers/admin/ai/self_hosted_models_controller.rb @@ -46,7 +46,9 @@ def update def destroy @self_hosted_model = ::Ai::SelfHostedModel.find(params[:id]) - if @self_hosted_model.destroy + result = ::Ai::SelfHostedModels::DestroyService.new(@self_hosted_model, current_user).execute + + if result.success? redirect_to admin_ai_self_hosted_models_url, notice: _("Self-Hosted Model was deleted") else render :index diff --git a/ee/app/graphql/mutations/ai/self_hosted_models/delete.rb b/ee/app/graphql/mutations/ai/self_hosted_models/delete.rb index c178f33e9a58ff090357de6406b0871274e30fcf..efb2e407d6cc95f783eeb5b13b6a7b605e8d0d51 100644 --- a/ee/app/graphql/mutations/ai/self_hosted_models/delete.rb +++ b/ee/app/graphql/mutations/ai/self_hosted_models/delete.rb @@ -15,28 +15,20 @@ class Delete < Base def resolve(**args) check_feature_access! - result = delete_self_hosted_model(args) - - if result[:errors].present? - { - self_hosted_model: nil, - errors: Array(result[:errors]) - } - else - { self_hosted_model: result, errors: [] } - end - end - - private - - def delete_self_hosted_model(args) model = find_object(id: args[:id]) return { errors: ["Self-hosted model not found"] } unless model - model.destroy + result = ::Ai::SelfHostedModels::DestroyService.new(model, current_user).execute + + { + self_hosted_model: result.success? ? result.payload : nil, + errors: result.error? ? Array.wrap(result.errors) : [] + } end + private + def find_object(id:) GitlabSchema.object_from_id(id, expected_type: ::Ai::SelfHostedModel).sync end diff --git a/ee/app/services/ai/self_hosted_models/destroy_service.rb b/ee/app/services/ai/self_hosted_models/destroy_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..b98c9ebde737c2ec07802219322c4e18ed664739 --- /dev/null +++ b/ee/app/services/ai/self_hosted_models/destroy_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Ai + module SelfHostedModels + class DestroyService + def initialize(self_hosted_model, user) + @self_hosted_model = self_hosted_model + @user = user + end + + def execute + if self_hosted_model.destroy + audit_destroy_event + + ServiceResponse.success(payload: self_hosted_model) + else + ServiceResponse.error(message: self_hosted_model.errors.full_messages.join(", ")) + end + end + + private + + attr_accessor :self_hosted_model, :user + + def audit_destroy_event + model = self_hosted_model + audit_context = { + name: 'self_hosted_model_destroyed', + author: user, + scope: user, + target: model, + message: "Self-hosted model #{model.name}/#{model.model}/#{model.endpoint} destroyed" + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end + end + end +end diff --git a/ee/spec/services/ai/self_hosted_models/destroy_service_spec.rb b/ee/spec/services/ai/self_hosted_models/destroy_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e37eb86d7d4c1f1b5979b2bf6a2170f7da45a0d8 --- /dev/null +++ b/ee/spec/services/ai/self_hosted_models/destroy_service_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Ai::SelfHostedModels::DestroyService, feature_category: :"self-hosted_models" do + let_it_be(:user) { create(:user) } + + let(:self_hosted_model) { create(:ai_self_hosted_model) } + + let(:service) { described_class.new(self_hosted_model, user) } + + let(:audit_event) do + model = self_hosted_model + { + name: 'self_hosted_model_destroyed', + author: user, + scope: user, + target: model, + message: "Self-hosted model #{model.name}/#{model.model}/#{model.endpoint} destroyed" + } + end + + describe '#execute', :aggregate_failures do + subject(:result) { service.execute } + + context 'when the model is successfully destroyed' do + it 'returns a success response' do + expect(Gitlab::Audit::Auditor).to receive(:audit).with(audit_event) + + expect { result }.to change { ::Ai::SelfHostedModel.count }.by(-1) + + expect(result).to be_success + expect(result.payload).to eq(self_hosted_model) + end + end + + context 'when the model fails to be destroyed' do + before do + allow(self_hosted_model).to receive(:destroy).and_return(false) + allow(self_hosted_model).to receive_message_chain(:errors, :full_messages).and_return(['Error message']) + end + + it 'returns an error response' do + expect(Gitlab::Audit::Auditor).not_to receive(:audit) + expect { result }.not_to change { ::Ai::SelfHostedModel.count } + + expect(result).to be_error + expect(result.message).to eq('Error message') + end + end + end +end