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 433fd7f611afcca1fdbc5b143cab6c445fdad12d..856ac4e75a0bc86bb08b5c5d4ecd246705785bc0 100644 --- a/ee/app/controllers/admin/ai/self_hosted_models_controller.rb +++ b/ee/app/controllers/admin/ai/self_hosted_models_controller.rb @@ -22,7 +22,7 @@ def ensure_registration! end def ensure_feature_enabled! - render_404 unless Ability.allowed?(current_user, :manage_ai_settings) + render_404 unless Ability.allowed?(current_user, :manage_self_hosted_models_settings) end end end diff --git a/ee/app/controllers/admin/ai/terms_and_conditions_controller.rb b/ee/app/controllers/admin/ai/terms_and_conditions_controller.rb index 31450d0fa440f04e051ad923a599a860ba63463a..7767e1269caab7e1a519b44038962f08b5c94d1f 100644 --- a/ee/app/controllers/admin/ai/terms_and_conditions_controller.rb +++ b/ee/app/controllers/admin/ai/terms_and_conditions_controller.rb @@ -25,7 +25,7 @@ def create private def ensure_feature_enabled! - render_404 unless Ability.allowed?(current_user, :manage_ai_settings) + render_404 unless Ability.allowed?(current_user, :manage_self_hosted_models_settings) end def audit_event(user) diff --git a/ee/app/graphql/mutations/ai/feature_settings/base.rb b/ee/app/graphql/mutations/ai/feature_settings/base.rb index 65231681197b095b764562a7219e7a87a920f082..d00e5de2fbf42e3bba92f4620d880f98c3ca6c48 100644 --- a/ee/app/graphql/mutations/ai/feature_settings/base.rb +++ b/ee/app/graphql/mutations/ai/feature_settings/base.rb @@ -13,7 +13,7 @@ class Base < BaseMutation private def check_feature_access! - raise_resource_not_available_error! unless Ability.allowed?(current_user, :manage_ai_settings) + raise_resource_not_available_error! unless Ability.allowed?(current_user, :manage_self_hosted_models_settings) end end # rubocop: enable GraphQL/GraphqlName diff --git a/ee/app/graphql/mutations/ai/self_hosted_models/base.rb b/ee/app/graphql/mutations/ai/self_hosted_models/base.rb index fe61191e10587bef4239e9fc08810a0f0aa889cb..294f4597e1c7aba8f4f5f83fec52807582ea237c 100644 --- a/ee/app/graphql/mutations/ai/self_hosted_models/base.rb +++ b/ee/app/graphql/mutations/ai/self_hosted_models/base.rb @@ -37,7 +37,7 @@ def self.arguments_for_model_attributes private def check_feature_access! - raise_resource_not_available_error! unless Ability.allowed?(current_user, :manage_ai_settings) + raise_resource_not_available_error! unless Ability.allowed?(current_user, :manage_self_hosted_models_settings) end end # rubocop: enable GraphQL/GraphqlName diff --git a/ee/app/graphql/resolvers/ai/feature_settings/feature_settings_resolver.rb b/ee/app/graphql/resolvers/ai/feature_settings/feature_settings_resolver.rb index fe41003003941c87d829511cbca68efbe125ebde..22227c109226c2f20310296c21900353bd964d12 100644 --- a/ee/app/graphql/resolvers/ai/feature_settings/feature_settings_resolver.rb +++ b/ee/app/graphql/resolvers/ai/feature_settings/feature_settings_resolver.rb @@ -14,7 +14,7 @@ class FeatureSettingsResolver < BaseResolver description: 'Global ID of the self-hosted model.' def resolve(self_hosted_model_id: nil) - raise_resource_not_available_error! unless Ability.allowed?(current_user, :manage_ai_settings) + return unless Ability.allowed?(current_user, :manage_self_hosted_models_settings) feature_settings = get_feature_settings(self_hosted_model_id) diff --git a/ee/app/graphql/resolvers/ai/self_hosted_models/self_hosted_models_resolver.rb b/ee/app/graphql/resolvers/ai/self_hosted_models/self_hosted_models_resolver.rb index d90c28b8c335041e44471ad3c44f01827200d69e..929dc084fc7ec4dc8ecc64db60d1d95af0c37d56 100644 --- a/ee/app/graphql/resolvers/ai/self_hosted_models/self_hosted_models_resolver.rb +++ b/ee/app/graphql/resolvers/ai/self_hosted_models/self_hosted_models_resolver.rb @@ -7,7 +7,7 @@ class SelfHostedModelsResolver < BaseResolver type ::Types::Ai::SelfHostedModels::SelfHostedModelType.connection_type, null: false def resolve(**args) - return unless Ability.allowed?(current_user, :manage_ai_settings) + return unless Ability.allowed?(current_user, :manage_self_hosted_models_settings) if args[:id] get_self_hosted_model(args[:id]) diff --git a/ee/app/policies/ee/global_policy.rb b/ee/app/policies/ee/global_policy.rb index e88979745810a4106981101af1e3e016aeb4e427..61400bbd670ef9f8cd93dc01997c4084bf9cd8ea 100644 --- a/ee/app/policies/ee/global_policy.rb +++ b/ee/app/policies/ee/global_policy.rb @@ -102,11 +102,12 @@ module GlobalPolicy ::License.feature_available?(:default_roles_assignees) end - condition(:user_allowed_to_manage_ai_settings) do + condition(:user_allowed_to_manage_self_hosted_models_settings) do next false if ::Feature.disabled?(:allow_self_hosted_features_for_com) && ::Gitlab::Saas.feature_available?(:gitlab_com_subscriptions) - ::License.current&.paid? # Replace with license :ai_self_hosted_model for GA + ::License.current&.ultimate? && # Replace with license :ai_self_hosted_model for GA + ::GitlabSubscriptions::AddOnPurchase.for_duo_enterprise.active.exists? end condition(:x_ray_available) do @@ -147,8 +148,8 @@ module GlobalPolicy enable :read_cloud_connector_status end - rule { admin & user_allowed_to_manage_ai_settings }.policy do - enable :manage_ai_settings + rule { admin & user_allowed_to_manage_self_hosted_models_settings }.policy do + enable :manage_self_hosted_models_settings end rule { admin & custom_roles_allowed }.policy do diff --git a/ee/spec/policies/global_policy_spec.rb b/ee/spec/policies/global_policy_spec.rb index 490daaa68011fa25fbc28f3720314a06659e44d5..83cfe5a78c5e0b0abb6c44e94347b3e674796d7c 100644 --- a/ee/spec/policies/global_policy_spec.rb +++ b/ee/spec/policies/global_policy_spec.rb @@ -829,7 +829,9 @@ describe 'manage self-hosted AI models' do let(:current_user) { admin } - let(:license_double) { instance_double('License', paid?: true) } + let(:license_double) { instance_double('License', ultimate?: true) } + + let_it_be(:add_on_purchase) { create(:gitlab_subscription_add_on_purchase, :duo_enterprise, :active) } before do allow(License).to receive(:current).and_return(license_double) @@ -837,17 +839,25 @@ context 'when admin' do context 'when conditions are respected', :enable_admin_mode do - it { is_expected.to be_allowed(:manage_ai_settings) } + it { is_expected.to be_allowed(:manage_self_hosted_models_settings) } end context 'when admin mode is disabled' do - it { is_expected.to be_disallowed(:manage_ai_settings) } + it { is_expected.to be_disallowed(:manage_self_hosted_models_settings) } + end + + context 'when license is not an Ultimate license', :enable_admin_mode do + let(:license_double) { instance_double('License', ultimate?: false) } + + it { is_expected.to be_disallowed(:manage_self_hosted_models_settings) } end - context 'when license is not paid', :enable_admin_mode do - let(:license_double) { instance_double('License', paid?: false) } + context 'when there is no active Duo Enterprise subscription', :enable_admin_mode do + before do + add_on_purchase.update!(expires_on: 1.day.ago) + end - it { is_expected.to be_disallowed(:manage_ai_settings) } + it { is_expected.to be_disallowed(:manage_self_hosted_models_settings) } end context 'when instance is in SASS mode', :enable_admin_mode do @@ -856,20 +866,20 @@ stub_feature_flags(allow_self_hosted_features_for_com: true) end - it { is_expected.to be_allowed(:manage_ai_settings) } + it { is_expected.to be_allowed(:manage_self_hosted_models_settings) } context 'when allow_self_hosted_features_for_com is disabled' do before do stub_feature_flags(allow_self_hosted_features_for_com: false) end - it { is_expected.to be_disallowed(:manage_ai_settings) } + it { is_expected.to be_disallowed(:manage_self_hosted_models_settings) } end end end context 'when regular user' do - it { is_expected.to be_disallowed(:manage_ai_settings) } + it { is_expected.to be_disallowed(:manage_self_hosted_models_settings) } end end diff --git a/ee/spec/requests/admin/ai/self_hosted_models_controller_spec.rb b/ee/spec/requests/admin/ai/self_hosted_models_controller_spec.rb index 63086f333bc385effe6645fd2eeac21f5702c4e8..a9dfcdda85db3b60d0fa876de0a5cf7d0a8b9386 100644 --- a/ee/spec/requests/admin/ai/self_hosted_models_controller_spec.rb +++ b/ee/spec/requests/admin/ai/self_hosted_models_controller_spec.rb @@ -5,6 +5,10 @@ RSpec.describe Admin::Ai::SelfHostedModelsController, :enable_admin_mode, feature_category: :"self-hosted_models" do let(:admin) { create(:admin) } let(:duo_features_enabled) { true } + let_it_be(:license) { create(:license, plan: License::ULTIMATE_PLAN) } + let_it_be(:add_on_purchase) do + create(:gitlab_subscription_add_on_purchase, :duo_enterprise, :active) + end before do sign_in(admin) @@ -31,7 +35,7 @@ context 'when the user is not authorized' do it 'performs the right authorization correctly' do allow(Ability).to receive(:allowed?).and_call_original - expect(Ability).to receive(:allowed?).with(admin, :manage_ai_settings).and_return(false) + expect(Ability).to receive(:allowed?).with(admin, :manage_self_hosted_models_settings).and_return(false) perform_request @@ -39,4 +43,21 @@ end end end + + describe 'GET #index' do + let(:page) { Nokogiri::HTML(response.body) } + + subject :perform_request do + get admin_ai_self_hosted_models_path + end + + it 'returns list of self-hosted models' do + perform_request + + expect(response).to have_gitlab_http_status(:ok) + end + + it_behaves_like 'returns 404' + it_behaves_like 'must accept terms and conditions' + end end diff --git a/ee/spec/requests/admin/ai/terms_and_conditions_controller_spec.rb b/ee/spec/requests/admin/ai/terms_and_conditions_controller_spec.rb index 06a491c4822c6b188fe5e1188e78defad23473fe..0fa8756562f6f3893c11c21043468ed995f02d49 100644 --- a/ee/spec/requests/admin/ai/terms_and_conditions_controller_spec.rb +++ b/ee/spec/requests/admin/ai/terms_and_conditions_controller_spec.rb @@ -5,6 +5,10 @@ RSpec.describe Admin::Ai::TermsAndConditionsController, :enable_admin_mode, feature_category: :"self-hosted_models" do let(:admin) { create(:admin) } let(:duo_features_enabled) { true } + let_it_be(:license) { create(:license, plan: License::ULTIMATE_PLAN) } + let_it_be(:add_on_purchase) do + create(:gitlab_subscription_add_on_purchase, :duo_enterprise, :active) + end before do sign_in(admin) @@ -15,7 +19,7 @@ context 'when the user is not authorized' do it 'performs the right authorization correctly' do allow(Ability).to receive(:allowed?).and_call_original - expect(Ability).to receive(:allowed?).with(admin, :manage_ai_settings).and_return(false) + expect(Ability).to receive(:allowed?).with(admin, :manage_self_hosted_models_settings).and_return(false) perform_request diff --git a/ee/spec/requests/api/graphql/ai/feature_settings/feature_settings_spec.rb b/ee/spec/requests/api/graphql/ai/feature_settings/feature_settings_spec.rb index a020353815160b22c36d59027db8d3d03bbc5f03..7b50a4248ababadde0dfcce9ef0294119a3624c5 100644 --- a/ee/spec/requests/api/graphql/ai/feature_settings/feature_settings_spec.rb +++ b/ee/spec/requests/api/graphql/ai/feature_settings/feature_settings_spec.rb @@ -6,6 +6,11 @@ include GraphqlHelpers let_it_be(:current_user) { create(:admin) } + let_it_be(:license) { create(:license, plan: License::ULTIMATE_PLAN) } + let_it_be(:add_on_purchase) do + create(:gitlab_subscription_add_on_purchase, :duo_enterprise, :active) + end + let(:query) do %( query aiFeatureSettings { @@ -64,14 +69,6 @@ allow(::Ai::FeatureSetting).to receive(:allowed_features).and_return(test_ai_feature_enum) end - shared_examples 'an error response' do |expected_error_message| - it 'returns an error', :aggregate_failures do - post_graphql(query, current_user: current_user) - expect(graphql_data['aiFeatureSettings']).to be_nil - expect_graphql_errors_to_include(expected_error_message) - end - end - context "when the user is authorized" do context 'when no query parameters are given' do let(:expected_response) do diff --git a/ee/spec/requests/api/graphql/ai/feature_settings/update_spec.rb b/ee/spec/requests/api/graphql/ai/feature_settings/update_spec.rb index 3a99ad6e6fc56b0b4a4476db5763019f8d59ad42..a959cf23eabb2988f2c11fb09adebe71099b629e 100644 --- a/ee/spec/requests/api/graphql/ai/feature_settings/update_spec.rb +++ b/ee/spec/requests/api/graphql/ai/feature_settings/update_spec.rb @@ -8,6 +8,10 @@ let_it_be(:current_user) { create(:admin) } let_it_be(:self_hosted_model) { create(:ai_self_hosted_model) } let_it_be(:feature_setting) { create(:ai_feature_setting, provider: :vendored, self_hosted_model: nil) } + let_it_be(:license) { create(:license, plan: License::ULTIMATE_PLAN) } + let_it_be(:add_on_purchase) do + create(:gitlab_subscription_add_on_purchase, :duo_enterprise, :active) + end let(:mutation_name) { :ai_feature_setting_update } let(:mutation_params) do diff --git a/ee/spec/requests/api/graphql/ai/self_hosted_models/connection_check_spec.rb b/ee/spec/requests/api/graphql/ai/self_hosted_models/connection_check_spec.rb index 7a58c1ffbacaa4b9e7482fcb4152793eb7ac00bc..cad00c3bac2533286efd186b95f19133a86a20b5 100644 --- a/ee/spec/requests/api/graphql/ai/self_hosted_models/connection_check_spec.rb +++ b/ee/spec/requests/api/graphql/ai/self_hosted_models/connection_check_spec.rb @@ -6,6 +6,10 @@ include GraphqlHelpers let_it_be(:current_user) { create(:admin) } + let_it_be(:license) { create(:license, plan: License::ULTIMATE_PLAN) } + let_it_be(:add_on_purchase) do + create(:gitlab_subscription_add_on_purchase, :duo_enterprise, :active) + end let(:input) do { @@ -22,12 +26,10 @@ subject(:request) { post_graphql_mutation(mutation, current_user: current_user) } - shared_examples 'it calls the manage_ai_settings policy' do - it 'calls the manage_ai_settings policy' do + shared_examples 'it calls the manage_self_hosted_models_settings policy' do + it 'calls the manage_self_hosted_models_settings policy' do allow(::Ability).to receive(:allowed?).and_call_original - - expect(::Ability).to receive(:allowed?) - .with(current_user, :manage_ai_settings) + expect(::Ability).to receive(:allowed?).with(current_user, :manage_self_hosted_models_settings) request end @@ -36,14 +38,14 @@ context 'when user is not allowed to write changes' do let(:current_user) { create(:user) } - it_behaves_like 'it calls the manage_ai_settings policy' + it_behaves_like 'it calls the manage_self_hosted_models_settings policy' it_behaves_like 'a mutation that returns a top-level access error' end context 'when user is allowed to write changes' do let(:probe_graphql_result) { mutation_response['result'] } - it_behaves_like 'it calls the manage_ai_settings policy' + it_behaves_like 'it calls the manage_self_hosted_models_settings policy' context 'when there are errors with creating the self-hosted model' do let(:error_message) { 'API error' } diff --git a/ee/spec/requests/api/graphql/ai/self_hosted_models/create_spec.rb b/ee/spec/requests/api/graphql/ai/self_hosted_models/create_spec.rb index 18fe597811f73024d798a700e54fce88fe33c275..02ce94e8249589f13a429e9b0facff6b317e4c06 100644 --- a/ee/spec/requests/api/graphql/ai/self_hosted_models/create_spec.rb +++ b/ee/spec/requests/api/graphql/ai/self_hosted_models/create_spec.rb @@ -6,6 +6,10 @@ include GraphqlHelpers let_it_be(:current_user) { create(:admin) } + let_it_be(:license) { create(:license, plan: License::ULTIMATE_PLAN) } + let_it_be(:add_on_purchase) do + create(:gitlab_subscription_add_on_purchase, :duo_enterprise, :active) + end let(:input) do { @@ -23,11 +27,9 @@ subject(:request) { post_graphql_mutation(mutation, current_user: current_user) } shared_examples 'it calls the manage_ai_settings policy' do - it 'calls the manage_ai_settings policy' do + it 'calls the manage_self_hosted_models_settings policy' do allow(::Ability).to receive(:allowed?).and_call_original - - expect(::Ability).to receive(:allowed?) - .with(current_user, :manage_ai_settings) + expect(::Ability).to receive(:allowed?).with(current_user, :manage_self_hosted_models_settings) request end diff --git a/ee/spec/requests/api/graphql/ai/self_hosted_models/delete_spec.rb b/ee/spec/requests/api/graphql/ai/self_hosted_models/delete_spec.rb index 20bbef9097fad7e2a4d8313f2224a855927a4a3a..d8ed56b639f01393db0604e7d87b8160277a69bc 100644 --- a/ee/spec/requests/api/graphql/ai/self_hosted_models/delete_spec.rb +++ b/ee/spec/requests/api/graphql/ai/self_hosted_models/delete_spec.rb @@ -6,6 +6,11 @@ include GraphqlHelpers let_it_be(:current_user) { create(:admin) } + let_it_be(:license) { create(:license, plan: License::ULTIMATE_PLAN) } + let_it_be(:add_on_purchase) do + create(:gitlab_subscription_add_on_purchase, :duo_enterprise, :active) + end + let_it_be(:self_hosted_model) do create( :ai_self_hosted_model, diff --git a/ee/spec/requests/api/graphql/ai/self_hosted_models/self_hosted_models_spec.rb b/ee/spec/requests/api/graphql/ai/self_hosted_models/self_hosted_models_spec.rb index 9382e8e74fac635e08325dc8896d831b802d4c6b..3864825fd7461fadd832f25de6fbaac35c5bd00c 100644 --- a/ee/spec/requests/api/graphql/ai/self_hosted_models/self_hosted_models_spec.rb +++ b/ee/spec/requests/api/graphql/ai/self_hosted_models/self_hosted_models_spec.rb @@ -6,6 +6,10 @@ include GraphqlHelpers let_it_be(:current_user) { create(:admin) } + let_it_be(:license) { create(:license, plan: License::ULTIMATE_PLAN) } + let_it_be(:add_on_purchase) do + create(:gitlab_subscription_add_on_purchase, :duo_enterprise, :active) + end let! :model_params do [ @@ -38,18 +42,18 @@ let(:query) do %( - query SelfHostedModel { - aiSelfHostedModels { - nodes { - id - name - model - modelDisplayName - endpoint - hasApiToken + query SelfHostedModel { + aiSelfHostedModels { + nodes { + id + name + model + modelDisplayName + endpoint + hasApiToken + } } } - } ) end @@ -84,18 +88,18 @@ let(:self_hosted_model_gid) { self_hosted_models.first.to_global_id } let(:query) do %( - query SelfHostedModel { - aiSelfHostedModels(id: "#{self_hosted_model_gid}") { - nodes { - id - name - model - modelDisplayName - endpoint - apiToken + query SelfHostedModel { + aiSelfHostedModels(id: "#{self_hosted_model_gid}") { + nodes { + id + name + model + modelDisplayName + endpoint + apiToken + } } } - } ) end diff --git a/ee/spec/requests/api/graphql/ai/self_hosted_models/update_spec.rb b/ee/spec/requests/api/graphql/ai/self_hosted_models/update_spec.rb index 1f7ec00c57b2accbdb0a35456f97a63187fc06dc..2a2c0dd687a255aea253cc3cea466f578115d27f 100644 --- a/ee/spec/requests/api/graphql/ai/self_hosted_models/update_spec.rb +++ b/ee/spec/requests/api/graphql/ai/self_hosted_models/update_spec.rb @@ -6,6 +6,11 @@ include GraphqlHelpers let_it_be(:current_user) { create(:admin) } + let_it_be(:license) { create(:license, plan: License::ULTIMATE_PLAN) } + let_it_be(:add_on_purchase) do + create(:gitlab_subscription_add_on_purchase, :duo_enterprise, :active) + end + let_it_be(:self_hosted_model) do create( :ai_self_hosted_model, diff --git a/ee/spec/support/shared_examples/graphql/ai/self_hosted_models/self_hosted_models_examples.rb b/ee/spec/support/shared_examples/graphql/ai/self_hosted_models/self_hosted_models_examples.rb index 09035bfc51a4f001e5f8e7b2996bcba996f24c14..250f4fe2c424f80e5de456163378cff7bdd9680e 100644 --- a/ee/spec/support/shared_examples/graphql/ai/self_hosted_models/self_hosted_models_examples.rb +++ b/ee/spec/support/shared_examples/graphql/ai/self_hosted_models/self_hosted_models_examples.rb @@ -5,7 +5,7 @@ RSpec.shared_examples 'performs the right authorization' do it 'performs the right authorization correctly' do allow(Ability).to receive(:allowed?).and_call_original - expect(Ability).to receive(:allowed?).with(current_user, :manage_ai_settings) + expect(Ability).to receive(:allowed?).with(current_user, :manage_self_hosted_models_settings) request end