diff --git a/config/audit_events/types/self_hosted_model_updated.yml b/config/audit_events/types/self_hosted_model_updated.yml new file mode 100644 index 0000000000000000000000000000000000000000..0746ff0004b68facaf0ab73a962f0ac2ea416847 --- /dev/null +++ b/config/audit_events/types/self_hosted_model_updated.yml @@ -0,0 +1,9 @@ +name: self_hosted_model_updated +description: A self-hosted model configuration was updated +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/483295 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165520 +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 c34c4aeabe3d82e2b9728a50013655640d04b293..01cbcfb726c9c7138dc8fad0ec976aa34c44347c 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -454,6 +454,7 @@ Audit event types belong to the following product categories. | Name | Description | Saved to database | Streamed | Introduced in | Scope | |:------------|:------------|:------------------|:---------|:--------------|:--------------| | [`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 | +| [`self_hosted_model_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165520) | A self-hosted model configuration was updated | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.4](https://gitlab.com/gitlab-org/gitlab/-/issues/483295) | Instance, User | ### Source code management 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..5e169fb676aa9ce59006e35cdd148798d03803ed 100644 --- a/ee/app/controllers/admin/ai/self_hosted_models_controller.rb +++ b/ee/app/controllers/admin/ai/self_hosted_models_controller.rb @@ -36,7 +36,11 @@ def edit def update @self_hosted_model = ::Ai::SelfHostedModel.find(params[:id]) - if @self_hosted_model.update(update_self_hosted_model_params) + result = ::Ai::SelfHostedModels::UpdateService.new( + @self_hosted_model, current_user, update_self_hosted_model_params + ).execute + + if result.success? redirect_to admin_ai_self_hosted_models_url, notice: _("Self-Hosted Model was updated") else render :edit diff --git a/ee/app/graphql/mutations/ai/self_hosted_models/update.rb b/ee/app/graphql/mutations/ai/self_hosted_models/update.rb index e8d11b4d85bc85de7c6e8b91bd98ddf378cea24a..1aee1f49ae0b3017189d4bc021286235cedd3a0b 100644 --- a/ee/app/graphql/mutations/ai/self_hosted_models/update.rb +++ b/ee/app/graphql/mutations/ai/self_hosted_models/update.rb @@ -31,28 +31,18 @@ class Update < Base def resolve(**args) check_feature_access! - model = update_self_hosted_model(args) - - if model.errors.present? - { - self_hosted_model: nil, - errors: Array(model.errors) - } - else - { self_hosted_model: model, errors: [] } - end - end - - private + model = find_object(id: args.delete(:id)) - def update_self_hosted_model(args) - model = find_object(id: args[:id]) + result = ::Ai::SelfHostedModels::UpdateService.new(model, current_user, args).execute - model.update(args.except(:id)) - - model + { + 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/update_service.rb b/ee/app/services/ai/self_hosted_models/update_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..be16590430442fea5479567435be61882ef657cd --- /dev/null +++ b/ee/app/services/ai/self_hosted_models/update_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Ai + module SelfHostedModels + class UpdateService + def initialize(self_hosted_model, user, update_params) + @self_hosted_model = self_hosted_model + @user = user + @params = update_params + end + + def execute + if self_hosted_model.update(params) + record_audit_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, :params + + def record_audit_event + model = self_hosted_model + audit_context = { + name: 'self_hosted_model_updated', + author: user, + scope: user, + target: model, + message: "Self-hosted model #{model.name}/#{model.model}/#{model.endpoint} updated" + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end + end + end +end diff --git a/ee/spec/services/ai/self_hosted_models/update_service_spec.rb b/ee/spec/services/ai/self_hosted_models/update_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2474eb06c65576e14507b5485eb1cc03ab8e6b97 --- /dev/null +++ b/ee/spec/services/ai/self_hosted_models/update_service_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Ai::SelfHostedModels::UpdateService, feature_category: :"self-hosted_models" do + let_it_be(:user) { create(:user) } + + let(:self_hosted_model) { create(:ai_self_hosted_model) } + + let(:params) { {} } + let(:service) { described_class.new(self_hosted_model, user, params) } + + let(:audit_event) do + model = self_hosted_model + { + name: 'self_hosted_model_updated', + author: user, + scope: user, + target: model, + message: "Self-hosted model new model name/#{model.model}/#{model.endpoint} updated" + } + end + + describe '#execute', :aggregate_failures do + subject(:result) { service.execute } + + context 'when the model is successfully updated' do + let(:params) { { name: "new model name" } } + + it 'returns a success response' do + expect(Gitlab::Audit::Auditor).to receive(:audit).with(audit_event).and_call_original + + result + + expect(self_hosted_model.reload.name).to eq("new model name") + + expect(result).to be_success + expect(result.payload).to eq(self_hosted_model) + end + end + + context 'when the model fails to be updated' do + let(:params) { { endpoint: nil } } + + it 'returns an error response' do + expect(Gitlab::Audit::Auditor).not_to receive(:audit) + + expect(result).to be_error + expect(result.message).to eq("Endpoint can't be blank, Endpoint must be a valid URL") + end + end + end +end