From 076ee29d309f9aed4297dbf11dc072e258427e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= <alejandro@gitlab.com> Date: Thu, 19 Sep 2024 16:00:34 -0400 Subject: [PATCH] Consolidate CC access check logic for ga features --- doc/development/cloud_connector/index.md | 16 +-- .../models/concerns/ai/user_authorizable.rb | 13 +- ee/app/policies/ee/ci/build_policy.rb | 14 +-- ee/app/policies/ee/global_policy.rb | 24 +--- ee/app/policies/ee/merge_request_policy.rb | 13 +- ee/app/policies/ee/project_policy.rb | 13 +- ee/app/policies/vulnerability_policy.rb | 14 +-- ee/lib/api/code_suggestions.rb | 6 +- .../gitlab/llm/utils/ai_features_catalogue.rb | 19 +++ ee/lib/gitlab/llm/utils/user_authorizer.rb | 46 ------- .../gitlab/llm/utils/user_authorizer_spec.rb | 101 --------------- .../concerns/ai/user_authorizable_spec.rb | 26 ++++ ee/spec/policies/ci/build_policy_spec.rb | 48 +------ ee/spec/policies/global_policy_spec.rb | 97 +++++---------- ee/spec/policies/merge_request_policy_spec.rb | 76 +++--------- ee/spec/policies/vulnerability_policy_spec.rb | 117 ++---------------- ee/spec/requests/api/code_suggestions_spec.rb | 66 +++++----- .../projects/generate_commit_message_spec.rb | 7 +- .../user_code_suggestions_available_spec.rb | 32 ++--- 19 files changed, 190 insertions(+), 558 deletions(-) delete mode 100644 ee/lib/gitlab/llm/utils/user_authorizer.rb delete mode 100644 ee/spec/lib/gitlab/llm/utils/user_authorizer_spec.rb diff --git a/doc/development/cloud_connector/index.md b/doc/development/cloud_connector/index.md index 78dce6b3319d9..aa775e7fc3310 100644 --- a/doc/development/cloud_connector/index.md +++ b/doc/development/cloud_connector/index.md @@ -285,7 +285,7 @@ To decide if the service is available or visible to the end user, we need to: # Returns true if service is allowed to be used. # # For provided user, it will check if user is assigned to a proper seat. - CloudConnector::AvailableServices.find_by_name(:new_feature).allowed_for?(current_user) + current_user.allowed_to_use?(:new_feature) ``` ###### Example @@ -306,22 +306,10 @@ Add a new policy rule in [ee/global_policy.rb](https://gitlab.com/gitlab-org/git end condition(:user_allowed_to_use_new_feature) do - # Returns whether the service is free to access (no addon purchases is required) - if new_feature_service_data.free_access? - # optionally check if the experimental (beta) toggle is enabled for SM - # check if the feature is enabled for a group/project - else - # Returns true if service is allowed to be used. - # For provided user, it will check if user is assigned to a proper seat. - new_feature_service_data.allowed_for?(@user) - end + @user.allowed_to_use?(:new_feature) end rule { new_feature_licensed & user_allowed_to_use_new_feature }.enable :access_new_feature - - def new_feature_service_data - CloudConnector::AvailableServices.find_by_name(:new_feature) - end ``` The request diff --git a/ee/app/models/concerns/ai/user_authorizable.rb b/ee/app/models/concerns/ai/user_authorizable.rb index 96f830505ddaa..26c93422c2b08 100644 --- a/ee/app/models/concerns/ai/user_authorizable.rb +++ b/ee/app/models/concerns/ai/user_authorizable.rb @@ -117,10 +117,11 @@ def allowed_to_use?(ai_feature, service_name: nil, licensed_feature: :ai_feature return true if service.add_on_purchases.assigned_to_user(self).any? # If the user doesn't have add-on purchases and the service isn't free, they don't have access - return false unless service.free_access? + return false if !service.free_access? || + (service.name == :self_hosted_models && Feature.enabled?(:self_hosted_models_beta_ended, self)) if Gitlab::Saas.feature_available?(:duo_chat_on_saas) - licensed_to_use_in_com?(service, feature_data[:maturity]) + licensed_to_use_in_com?(feature_data[:maturity]) else licensed_to_use_in_sm?(licensed_feature) end @@ -128,8 +129,10 @@ def allowed_to_use?(ai_feature, service_name: nil, licensed_feature: :ai_feature private - def licensed_to_use_in_com?(ai_action, maturity) - requiring_seat = member_namespaces.with_ai_supported_plan.select do |namespace| + def licensed_to_use_in_com?(maturity) + with_plan = member_namespaces.with_ai_supported_plan + + requiring_seat = with_plan.select do |namespace| ::Feature.enabled?(:duo_chat_requires_licensed_seat, namespace) end @@ -137,7 +140,7 @@ def licensed_to_use_in_com?(ai_action, maturity) # to the user, they don't have access return false if requiring_seat.any? - maturity == :ga ? any_group_with_ga_ai_available?(ai_action) : any_group_with_ai_available? + maturity == :ga ? with_plan.any? : any_group_with_ai_available? end def licensed_to_use_in_sm?(licensed_feature) diff --git a/ee/app/policies/ee/ci/build_policy.rb b/ee/app/policies/ee/ci/build_policy.rb index 88f62943a4fb8..5c4664a38da99 100644 --- a/ee/app/policies/ee/ci/build_policy.rb +++ b/ee/app/policies/ee/ci/build_policy.rb @@ -8,15 +8,7 @@ module BuildPolicy prepended do # Authorize access to the troubleshoot job to Cloud Connector Service condition(:troubleshoot_job_cloud_connector_authorized) do - next true if troubleshoot_job_connection.allowed_for?(@user) - - next false unless troubleshoot_job_connection.free_access? - - if ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) # check if we are on SaaS - user&.any_group_with_ga_ai_available?(:troubleshoot_job) - else - License.feature_available?(:ai_features) - end + @user.allowed_to_use?(:troubleshoot_job) end # Authorize access to Troubleshoot Job @@ -37,10 +29,6 @@ module BuildPolicy troubleshoot_job_cloud_connector_authorized & troubleshoot_job_with_ai_authorized end.enable(:troubleshoot_job_with_ai) - - def troubleshoot_job_connection - CloudConnector::AvailableServices.find_by_name(:troubleshoot_job) - end end end end diff --git a/ee/app/policies/ee/global_policy.rb b/ee/app/policies/ee/global_policy.rb index e733562f17612..56f44a80c089f 100644 --- a/ee/app/policies/ee/global_policy.rb +++ b/ee/app/policies/ee/global_policy.rb @@ -61,30 +61,22 @@ module GlobalPolicy condition(:code_suggestions_enabled_for_user) do next false unless @user - next true if CloudConnector::AvailableServices.find_by_name(:code_suggestions).allowed_for?(@user) + next true if @user.allowed_to_use?(:code_suggestions) next false unless ::Ai::FeatureSetting.code_suggestions_self_hosted? - self_hosted_models_available_for?(@user) + @user.allowed_to_use?(:code_suggestions, service_name: :self_hosted_models) end - condition(:glab_ask_git_command_licensed) do - if ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) # check if we are on SaaS - next @user.any_group_with_ga_ai_available?(:glab_ask_git_command) - end - - next false unless ::Gitlab::CurrentSettings.duo_features_enabled? - - ::License.feature_available?(:glab_ask_git_command) + condition(:glab_ask_git_command_enabled) do + ::Gitlab::CurrentSettings.duo_features_enabled? end condition(:user_allowed_to_use_glab_ask_git_command) do - next true if glab_ask_git_command_data.free_access? - - glab_ask_git_command_data.allowed_for?(@user) + @user.allowed_to_use?(:glab_ask_git_command, licensed_feature: :glab_ask_git_command) end - rule { glab_ask_git_command_licensed & user_allowed_to_use_glab_ask_git_command }.policy do + rule { glab_ask_git_command_enabled & user_allowed_to_use_glab_ask_git_command }.policy do enable :access_glab_ask_git_command end @@ -219,10 +211,6 @@ module GlobalPolicy end end - def glab_ask_git_command_data - CloudConnector::AvailableServices.find_by_name(:glab_ask_git_command) - end - def duo_chat_free_access_was_cut_off? if ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) duo_chat_free_access_was_cut_off_for_gitlab_com? diff --git a/ee/app/policies/ee/merge_request_policy.rb b/ee/app/policies/ee/merge_request_policy.rb index 6ac3f91057856..c7f71916d7c5e 100644 --- a/ee/app/policies/ee/merge_request_policy.rb +++ b/ee/app/policies/ee/merge_request_policy.rb @@ -63,18 +63,8 @@ module MergeRequestPolicy subject.project.project_setting.duo_features_enabled? end - condition(:generate_commit_message_licensed) do - if ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) # check if we are on SaaS - next @user.any_group_with_ga_ai_available?(:generate_commit_message) - end - - ::License.feature_available?(:generate_commit_message) - end - condition(:user_allowed_to_use_generate_commit_message) do - next true if generate_commit_message_data.free_access? - - generate_commit_message_data.allowed_for?(@user) + @user.allowed_to_use?(:generate_commit_message, licensed_feature: :generate_commit_message) end def read_only? @@ -120,7 +110,6 @@ def group_access?(protected_branch) rule do generate_commit_message_enabled & - generate_commit_message_licensed & user_allowed_to_use_generate_commit_message end.enable :access_generate_commit_message end diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb index 1977dc1165ce9..a2ec855d7ccae 100644 --- a/ee/app/policies/ee/project_policy.rb +++ b/ee/app/policies/ee/project_policy.rb @@ -468,11 +468,18 @@ module ProjectPolicy enable :read_vulnerability_scanner end - condition(:resolve_vulnerability_authorized) do - ::Gitlab::Llm::Utils::UserAuthorizer.new(@user, subject, :resolve_vulnerability).allowed? + condition(:resolve_vulnerability_allowed) do + ::Gitlab::Llm::FeatureAuthorizer.new( + container: subject, + feature_name: :resolve_vulnerability + ).allowed? + end + + condition(:resolve_vulnerability_enabled) do + @user.allowed_to_use?(:resolve_vulnerability) end - rule { can?(:read_security_resource) & resolve_vulnerability_authorized }.policy do + rule { can?(:read_security_resource) & resolve_vulnerability_allowed & resolve_vulnerability_enabled }.policy do enable :resolve_vulnerability_with_ai end diff --git a/ee/app/policies/vulnerability_policy.rb b/ee/app/policies/vulnerability_policy.rb index 079ce7e32dc41..39a8bdf181b32 100644 --- a/ee/app/policies/vulnerability_policy.rb +++ b/ee/app/policies/vulnerability_policy.rb @@ -25,15 +25,7 @@ class VulnerabilityPolicy < BasePolicy # Authorize access to the Explain Vulnerability Cloud Connector Service condition(:explain_vulnerability_cloud_connector_authorized) do - next true if explain_vulnerability_service.allowed_for?(@user) - - next false unless explain_vulnerability_service.free_access? - - if ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) # check if we are on SaaS - @user.any_group_with_ga_ai_available?(:explain_vulnerability) - else - License.feature_available?(:ai_features) - end + @user.allowed_to_use?(:explain_vulnerability) end rule do @@ -45,8 +37,4 @@ class VulnerabilityPolicy < BasePolicy rule { can?(:admin_vulnerability) }.enable :create_external_issue_link rule { project.security_dashboard_enabled & can?(:developer_access) }.enable :create_external_issue_link - - def explain_vulnerability_service - CloudConnector::AvailableServices.find_by_name(:explain_vulnerability) - end end diff --git a/ee/lib/api/code_suggestions.rb b/ee/lib/api/code_suggestions.rb index 134f483b22a3e..31239111a565e 100644 --- a/ee/lib/api/code_suggestions.rb +++ b/ee/lib/api/code_suggestions.rb @@ -96,11 +96,7 @@ def saas_headers service = CloudConnector::AvailableServices.find_by_name(task.feature_name) - if Feature.enabled?(:self_hosted_models_beta_ended, current_user) - unauthorized! unless service.allowed_for?(current_user) - else - unauthorized! unless service.free_access? || service.allowed_for?(current_user) - end + unauthorized! unless current_user.allowed_to_use?(:code_suggestions, service_name: task.feature_name) token = service.access_token(current_user) unauthorized! if token.nil? diff --git a/ee/lib/gitlab/llm/utils/ai_features_catalogue.rb b/ee/lib/gitlab/llm/utils/ai_features_catalogue.rb index 20e3946c42745..dd4ae0de0fe99 100644 --- a/ee/lib/gitlab/llm/utils/ai_features_catalogue.rb +++ b/ee/lib/gitlab/llm/utils/ai_features_catalogue.rb @@ -122,7 +122,26 @@ class AiFeaturesCatalogue maturity: :ga, self_managed: true, internal: true + }, + code_suggestions: { + service_class: nil, + prompt_class: nil, + feature_category: :continuous_integration, + execute_method: nil, + maturity: :ga, + self_managed: true, + internal: true + }, + troubleshoot_job: { + service_class: nil, + prompt_class: nil, + feature_category: :code_suggestions, + execute_method: nil, + maturity: :ga, + self_managed: true, + internal: true } + }.freeze def self.external diff --git a/ee/lib/gitlab/llm/utils/user_authorizer.rb b/ee/lib/gitlab/llm/utils/user_authorizer.rb deleted file mode 100644 index 7bf9a38306783..0000000000000 --- a/ee/lib/gitlab/llm/utils/user_authorizer.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Llm - module Utils - class UserAuthorizer - def initialize(user, project, feature_name) - @user = user - @project = project - @feature_name = feature_name - end - - def allowed? - return false unless @user - - project_authorized? && user_authorized? - end - - private - - def project_authorized? - ::Gitlab::Llm::FeatureAuthorizer.new( - container: @project, - feature_name: @feature_name - ).allowed? - end - - def user_authorized? - return true if service.allowed_for?(@user) - - return false unless service.free_access? - - if ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) - @user.any_group_with_ga_ai_available?(@feature_name) - else - ::License.feature_available?(:ai_features) - end - end - - def service - @service ||= ::CloudConnector::AvailableServices.find_by_name(@feature_name) - end - end - end - end -end diff --git a/ee/spec/lib/gitlab/llm/utils/user_authorizer_spec.rb b/ee/spec/lib/gitlab/llm/utils/user_authorizer_spec.rb deleted file mode 100644 index c038f60073fb4..0000000000000 --- a/ee/spec/lib/gitlab/llm/utils/user_authorizer_spec.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Llm::Utils::UserAuthorizer, feature_category: :ai_abstraction_layer do - describe '#allowed?' do - subject { described_class.new(user, project, feature_name).allowed? } - - let_it_be(:project) { create(:project) } - let_it_be(:user) { create(:user) } - let(:feature_name) { :resolve_vulnerability } - let(:cloud_connector_free_access) { true } - let(:cloud_connector_user_access) { false } - let(:feature_authorizer) { instance_double(::Gitlab::Llm::FeatureAuthorizer) } - - before_all do - project.add_developer(user) - end - - context 'when user is present' do - before do - stub_licensed_features(ai_features: true) - allow(::CloudConnector::AvailableServices).to receive_message_chain(:find_by_name, - :free_access?).and_return(cloud_connector_free_access) - allow(::CloudConnector::AvailableServices).to receive_message_chain(:find_by_name, - :allowed_for?).and_return(cloud_connector_user_access) - allow(::Gitlab::Llm::FeatureAuthorizer).to receive(:new).and_return(feature_authorizer) - end - - context 'when feature is authorized' do - before do - allow(feature_authorizer).to receive(:allowed?).and_return(true) - end - - it { is_expected.to be true } - - context 'when feature is not licensed' do - before do - stub_licensed_features(ai_features: false) - end - - it { is_expected.to be false } - end - - describe 'cloud connector' do - using RSpec::Parameterized::TableSyntax - where(:free_access, :user_access, :allowed) do - true | true | true - true | false | true - false | true | true - false | false | false - end - - with_them do - let(:cloud_connector_free_access) { free_access } - let(:cloud_connector_user_access) { user_access } - - it { is_expected.to be allowed } - end - end - - context 'when on .org or .com', :saas do - using RSpec::Parameterized::TableSyntax - where(:group_with_ai_membership, :free_access, :user_access, :allowed) do - true | true | true | true - true | false | true | true - false | false | true | true - false | false | false | false - true | true | false | true - false | true | false | false - end - - with_them do - before do - allow(user).to receive(:any_group_with_ga_ai_available?).and_return(group_with_ai_membership) - end - - let(:cloud_connector_free_access) { free_access } - let(:cloud_connector_user_access) { user_access } - - it { is_expected.to be allowed } - end - end - end - - context 'when feature is not authorized' do - before do - allow(feature_authorizer).to receive(:allowed?).and_return(false) - end - - it { is_expected.to be false } - end - end - - context 'when user is not present' do - let_it_be(:user) { nil } - - it { is_expected.to be false } - end - end -end diff --git a/ee/spec/models/concerns/ai/user_authorizable_spec.rb b/ee/spec/models/concerns/ai/user_authorizable_spec.rb index bdb3913e18d79..31ea8fb31dad2 100644 --- a/ee/spec/models/concerns/ai/user_authorizable_spec.rb +++ b/ee/spec/models/concerns/ai/user_authorizable_spec.rb @@ -163,6 +163,32 @@ it_behaves_like 'when checking licensed features' + context 'when specifying a service name' do + let(:service_name) { :my_service } + + before do + stub_licensed_features(ai_features: true) + end + + subject { user.allowed_to_use?(ai_feature, service_name: service_name) } + + it { is_expected.to be true } + + context 'when service_name is self_hosted_models' do + let(:service_name) { :self_hosted_models } + + it { is_expected.to be false } + + context 'when self_hosted_models_beta_ended is disabled' do + before do + stub_feature_flags(self_hosted_models_beta_ended: false) + end + + it { is_expected.to be true } + end + end + end + context 'when specifying a licensed feature name' do it_behaves_like 'when checking licensed features' do let(:licensed_feature) { :generate_commit_message } diff --git a/ee/spec/policies/ci/build_policy_spec.rb b/ee/spec/policies/ci/build_policy_spec.rb index 6e9f048ae8ec9..c82207c44dae3 100644 --- a/ee/spec/policies/ci/build_policy_spec.rb +++ b/ee/spec/policies/ci/build_policy_spec.rb @@ -7,7 +7,6 @@ describe 'troubleshoot_job_with_ai' do let(:authorized) { true } - let(:cloud_connector_free_access) { true } let(:cloud_connector_user_access) { true } let_it_be(:project) { create(:project, :private) } let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project) } @@ -28,13 +27,7 @@ allow(::Gitlab::Llm::StageCheck).to receive(:available?).and_return(true) allow(user).to receive(:can?).with(:access_duo_chat).and_return(true) allow(user).to receive(:can?).with(:access_duo_features, build.project).and_return(true) - allow(::CloudConnector::AvailableServices).to receive(:find_by_name).with(:troubleshoot_job).and_return( - instance_double( - CloudConnector::BaseAvailableServiceData, - free_access?: cloud_connector_free_access, - allowed_for?: cloud_connector_user_access - ) - ) + allow(user).to receive(:allowed_to_use?).and_return(cloud_connector_user_access) end context 'when feature is chat authorized' do @@ -88,49 +81,20 @@ it { is_expected.to be_disallowed(:troubleshoot_job_with_ai) } end - # TODO: remove these tests when implementing https://gitlab.com/gitlab-org/gitlab/-/issues/473087 - describe 'cloud connector' do - using RSpec::Parameterized::TableSyntax - where(:free_access, :user_access, :allowed) do - true | true | true - true | false | true - false | true | true - false | false | false - end - - with_them do - let(:cloud_connector_free_access) { free_access } - let(:cloud_connector_user_access) { user_access } - let(:policy) { :troubleshoot_job_with_ai } - - it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } - end - end - context 'when on .org or .com', :saas do using RSpec::Parameterized::TableSyntax - where(:group_with_ai_membership, :free_access, :user_access, :licensed, :allowed) do - true | true | true | true | true - true | false | true | true | true - false | false | true | true | true - false | false | false | true | false - true | true | false | true | true - false | true | false | true | false - true | true | true | false | false - true | false | true | false | false - false | false | true | false | false - false | false | false | false | false - true | true | false | false | false - false | true | false | false | false + where(:user_access, :licensed, :allowed) do + true | true | true + true | false | false + false | true | false + false | false | false end with_them do before do - allow(user).to receive(:any_group_with_ga_ai_available?).and_return(group_with_ai_membership) allow(project).to receive(:licensed_feature_available?).with(:troubleshoot_job).and_return(licensed) end - let(:cloud_connector_free_access) { free_access } let(:cloud_connector_user_access) { user_access } let(:policy) { :troubleshoot_job_with_ai } diff --git a/ee/spec/policies/global_policy_spec.rb b/ee/spec/policies/global_policy_spec.rb index 7a79468a09d02..a7c408e40ddb6 100644 --- a/ee/spec/policies/global_policy_spec.rb +++ b/ee/spec/policies/global_policy_spec.rb @@ -604,35 +604,29 @@ let_it_be_with_reload(:current_user) { create(:user) } where(:code_suggestions_licensed, :duo_pro_seat_assigned, :self_hosted_enabled, :self_hosted_licensed, - :self_hosted_free_access, :self_hosted_beta_ended, :code_suggestions_enabled_for_user) do - true | true | true | true | false | false | be_allowed(:access_code_suggestions) - true | true | false | false | false | false | be_allowed(:access_code_suggestions) - true | false | true | true | false | false | be_allowed(:access_code_suggestions) - true | false | true | false | true | false | be_allowed(:access_code_suggestions) - true | false | true | false | true | true | be_disallowed(:access_code_suggestions) - true | false | true | false | false | false | be_disallowed(:access_code_suggestions) - true | false | true | false | false | false | be_disallowed(:access_code_suggestions) - true | false | false | false | false | false | be_disallowed(:access_code_suggestions) - false | true | true | true | false | false | be_disallowed(:access_code_suggestions) - false | false | false | false | false | false | be_disallowed(:access_code_suggestions) + :code_suggestions_enabled_for_user) do + true | true | true | true | be_allowed(:access_code_suggestions) + true | true | false | false | be_allowed(:access_code_suggestions) + true | false | true | true | be_allowed(:access_code_suggestions) + true | false | true | false | be_disallowed(:access_code_suggestions) + true | false | false | false | be_disallowed(:access_code_suggestions) + false | true | true | true | be_disallowed(:access_code_suggestions) + false | false | false | false | be_disallowed(:access_code_suggestions) end with_them do before do stub_licensed_features(code_suggestions: code_suggestions_licensed) - stub_feature_flags(self_hosted_models_beta_ended: self_hosted_beta_ended) - code_suggestions_service_data = instance_double(CloudConnector::BaseAvailableServiceData) - allow(CloudConnector::AvailableServices).to receive(:find_by_name).with(:code_suggestions) - .and_return(code_suggestions_service_data) - allow(code_suggestions_service_data).to receive(:allowed_for?).with(current_user) - .and_return(duo_pro_seat_assigned) + + # This is needed to allow responding differently based on the arguments. We make the default throw an error + # to check we don't get unexpected calls. + # See https://rspec.info/features/3-12/rspec-mocks/setting-constraints/matching-arguments/ + allow(current_user).to receive(:allowed_to_use?).and_raise(StandardError) + + allow(current_user).to receive(:allowed_to_use?).with(:code_suggestions).and_return(duo_pro_seat_assigned) allow(::Ai::FeatureSetting).to receive(:code_suggestions_self_hosted?).and_return(:self_hosted_enabled) - self_hosted_models_service_data = instance_double(CloudConnector::BaseAvailableServiceData) - allow(CloudConnector::AvailableServices).to receive(:find_by_name).with(:self_hosted_models) - .and_return(self_hosted_models_service_data) - allow(self_hosted_models_service_data).to receive(:allowed_for?).with(current_user) - .and_return(self_hosted_licensed) - allow(self_hosted_models_service_data).to receive(:free_access?).and_return(self_hosted_free_access) + allow(current_user).to receive(:allowed_to_use?) + .with(:code_suggestions, service_name: :self_hosted_models).and_return(self_hosted_licensed) end it { is_expected.to code_suggestions_enabled_for_user } @@ -809,56 +803,21 @@ describe 'explain git commands' do let(:policy) { :access_glab_ask_git_command } - context 'for self-managed' do - where(:duo_features_enabled, :licensed, :free_access, :allowed_for, :enabled_for_user) do - true | false | false | false | be_disallowed(:access_glab_ask_git_command) - true | true | false | false | be_disallowed(:access_glab_ask_git_command) - false | true | true | true | be_disallowed(:access_glab_ask_git_command) - true | true | false | true | be_allowed(:access_glab_ask_git_command) - true | true | true | false | be_allowed(:access_glab_ask_git_command) - true | true | true | true | be_allowed(:access_glab_ask_git_command) - end - - with_them do - before do - stub_licensed_features(glab_ask_git_command: licensed) - stub_application_setting(duo_features_enabled: duo_features_enabled) + where(:duo_features_enabled, :allowed_to_use, :enabled_for_user) do + true | false | be_disallowed(:access_glab_ask_git_command) + false | true | be_disallowed(:access_glab_ask_git_command) + true | true | be_allowed(:access_glab_ask_git_command) + end - service_data = CloudConnector::SelfManaged::AvailableServiceData.new(:glab_ask_git_command, nil, nil) - allow(CloudConnector::AvailableServices).to receive(:find_by_name) - .with(:glab_ask_git_command) - .and_return(service_data) - allow(service_data).to receive(:allowed_for?).with(current_user).and_return(allowed_for) - allow(service_data).to receive(:free_access?).and_return(free_access) - end + with_them do + before do + stub_application_setting(duo_features_enabled: duo_features_enabled) - it { is_expected.to enabled_for_user } + allow(current_user).to receive(:allowed_to_use?) + .with(:glab_ask_git_command, licensed_feature: :glab_ask_git_command).and_return(allowed_to_use) end - context 'for SaaS', :saas do - where(:free_access, :any_group_with_ga_ai_available, :allowed_for, :enabled_for_user) do - false | false | false | be_disallowed(:access_glab_ask_git_command) - true | false | false | be_disallowed(:access_glab_ask_git_command) - false | false | true | be_disallowed(:access_glab_ask_git_command) - true | true | false | be_allowed(:access_glab_ask_git_command) - true | true | true | be_allowed(:access_glab_ask_git_command) - end - - with_them do - before do - service_data = CloudConnector::SelfManaged::AvailableServiceData.new(:glab_ask_git_command, nil, nil) - allow(CloudConnector::AvailableServices).to receive(:find_by_name) - .with(:glab_ask_git_command) - .and_return(service_data) - allow(service_data).to receive(:allowed_for?).with(current_user).and_return(allowed_for) - allow(service_data).to receive(:free_access?).and_return(free_access) - allow(current_user).to receive(:any_group_with_ga_ai_available?) - .and_return(any_group_with_ga_ai_available) - end - - it { is_expected.to enabled_for_user } - end - end + it { is_expected.to enabled_for_user } end end diff --git a/ee/spec/policies/merge_request_policy_spec.rb b/ee/spec/policies/merge_request_policy_spec.rb index 2fa894d657d0f..97b8921fed272 100644 --- a/ee/spec/policies/merge_request_policy_spec.rb +++ b/ee/spec/policies/merge_request_policy_spec.rb @@ -417,70 +417,26 @@ def policy_for(user) subject(:policy) { policy_for(user) } - context 'for self-managed' do - where(:flag_enabled, :duo_features_enabled, :licensed, :free_access, :allowed_for, :enabled_for_user) do - false | true | false | false | false | be_disallowed(:access_generate_commit_message) - true | true | false | false | false | be_disallowed(:access_generate_commit_message) - true | true | true | false | false | be_disallowed(:access_generate_commit_message) - true | false | true | true | true | be_disallowed(:access_generate_commit_message) - true | true | true | false | true | be_allowed(:access_generate_commit_message) - true | true | true | true | false | be_allowed(:access_generate_commit_message) - true | true | true | true | true | be_allowed(:access_generate_commit_message) - end - - with_them do - before do - stub_licensed_features(generate_commit_message: licensed) - stub_feature_flags(generate_commit_message_flag: flag_enabled) - - allow(project) - .to receive_message_chain(:project_setting, :duo_features_enabled?) - .and_return(duo_features_enabled) - - service_data = CloudConnector::SelfManaged::AvailableServiceData.new(:generate_commit_message, nil, nil) - allow(CloudConnector::AvailableServices).to receive(:find_by_name) - .with(:generate_commit_message) - .and_return(service_data) - allow(service_data).to receive(:allowed_for?).with(user).and_return(allowed_for) - allow(service_data).to receive(:free_access?).and_return(free_access) - end - - it { is_expected.to enabled_for_user } - end + where(:flag_enabled, :duo_features_enabled, :allowed_to_use, :enabled_for_user) do + false | true | true | be_disallowed(:access_generate_commit_message) + true | true | false | be_disallowed(:access_generate_commit_message) + true | false | true | be_disallowed(:access_generate_commit_message) + true | true | true | be_allowed(:access_generate_commit_message) + end - context 'for SaaS', :saas do - where(:flag_enabled, :duo_features_enabled, :free_access, :any_group_with_ga_ai_available, :allowed_for, :enabled_for_user) do - false | true | false | false | false | be_disallowed(:access_generate_commit_message) - true | true | false | false | false | be_disallowed(:access_generate_commit_message) - true | true | true | false | false | be_disallowed(:access_generate_commit_message) - true | true | false | false | false | be_disallowed(:access_generate_commit_message) - true | true | false | false | true | be_disallowed(:access_generate_commit_message) - true | false | true | true | true | be_disallowed(:access_generate_commit_message) - true | true | true | true | false | be_allowed(:access_generate_commit_message) - true | true | true | true | true | be_allowed(:access_generate_commit_message) - end + with_them do + before do + stub_feature_flags(generate_commit_message_flag: flag_enabled) - with_them do - before do - stub_feature_flags(generate_commit_message_flag: flag_enabled) - - allow(project) - .to receive_message_chain(:project_setting, :duo_features_enabled?) - .and_return(duo_features_enabled) - - service_data = CloudConnector::SelfManaged::AvailableServiceData.new(:generate_commit_message, nil, nil) - allow(CloudConnector::AvailableServices).to receive(:find_by_name) - .with(:generate_commit_message) - .and_return(service_data) - allow(service_data).to receive(:allowed_for?).with(user).and_return(allowed_for) - allow(service_data).to receive(:free_access?).and_return(free_access) - allow(user).to receive(:any_group_with_ga_ai_available?) - .and_return(any_group_with_ga_ai_available) - end + allow(project) + .to receive_message_chain(:project_setting, :duo_features_enabled?) + .and_return(duo_features_enabled) - it { is_expected.to enabled_for_user } - end + allow(user).to receive(:allowed_to_use?) + .with(:generate_commit_message, licensed_feature: :generate_commit_message).and_return(allowed_to_use) end + + it { is_expected.to enabled_for_user } end end end diff --git a/ee/spec/policies/vulnerability_policy_spec.rb b/ee/spec/policies/vulnerability_policy_spec.rb index b9e73d5ececdd..fc4fa832eb828 100644 --- a/ee/spec/policies/vulnerability_policy_spec.rb +++ b/ee/spec/policies/vulnerability_policy_spec.rb @@ -79,12 +79,10 @@ end describe 'explain_vulnerability_with_ai' do - let(:authorized) { true } - let(:cloud_connector_free_access) { true } - let(:cloud_connector_user_access) { false } + let(:cloud_connector_user_access) { true } before do - stub_licensed_features(security_dashboard: true, ai_features: true) + stub_licensed_features(security_dashboard: true) project.add_developer(user) allow(::Gitlab::Llm::Chain::Utils::ChatAuthorizer).to receive_message_chain(:resource, :allowed?).and_return(authorized) @@ -93,10 +91,7 @@ allow(user).to receive(:can?).with("read_vulnerability", vulnerability).and_return(true) allow(user).to receive(:can?).with(:access_duo_chat).and_return(true) allow(user).to receive(:can?).with(:access_duo_features, vulnerability.project).and_return(true) - allow(::CloudConnector::AvailableServices).to receive_message_chain(:find_by_name, - :free_access?).and_return(cloud_connector_free_access) - allow(::CloudConnector::AvailableServices).to receive_message_chain(:find_by_name, - :allowed_for?).and_return(cloud_connector_user_access) + allow(user).to receive(:allowed_to_use?).and_return(cloud_connector_user_access) end context 'when feature is authorized' do @@ -121,10 +116,8 @@ it { is_expected.to be_disallowed(:explain_vulnerability_with_ai) } end - context 'when feature is not licensed' do - before do - stub_licensed_features(ai_features: false) - end + context 'when service is not authorized' do + let(:cloud_connector_user_access) { false } it { is_expected.to be_disallowed(:explain_vulnerability_with_ai) } end @@ -135,62 +128,16 @@ it { is_expected.to be_disallowed(:explain_vulnerability_with_ai) } end - - describe 'cloud connector' do - using RSpec::Parameterized::TableSyntax - where(:free_access, :user_access, :allowed) do - true | true | true - true | false | true - false | true | true - false | false | false - end - - with_them do - let(:cloud_connector_free_access) { free_access } - let(:cloud_connector_user_access) { user_access } - let(:policy) { :explain_vulnerability_with_ai } - - it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } - end - end - - context 'when on .org or .com', :saas do - using RSpec::Parameterized::TableSyntax - where(:group_with_ai_membership, :free_access, :user_access, :allowed) do - true | true | true | true - true | false | true | true - false | false | true | true - false | false | false | false - true | true | false | true - false | true | false | false - end - - with_them do - before do - allow(user).to receive(:any_group_with_ga_ai_available?).and_return(group_with_ai_membership) - end - - let(:cloud_connector_free_access) { free_access } - let(:cloud_connector_user_access) { user_access } - let(:policy) { :explain_vulnerability_with_ai } - - it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } - end - end end describe 'resolve_vulnerability_with_ai' do let(:authorizer) { instance_double(::Gitlab::Llm::FeatureAuthorizer) } - let(:cloud_connector_free_access) { true } - let(:cloud_connector_user_access) { false } + let(:cloud_connector_user_access) { true } before do - stub_licensed_features(security_dashboard: true, ai_features: true) + stub_licensed_features(security_dashboard: true) project.add_developer(user) - allow(::CloudConnector::AvailableServices).to receive_message_chain(:find_by_name, - :free_access?).and_return(cloud_connector_free_access) - allow(::CloudConnector::AvailableServices).to receive_message_chain(:find_by_name, - :allowed_for?).and_return(cloud_connector_user_access) + allow(user).to receive(:allowed_to_use?).and_return(cloud_connector_user_access) allow(::Gitlab::Llm::FeatureAuthorizer).to receive(:new).and_return(authorizer) end @@ -209,55 +156,11 @@ it { is_expected.to be_disallowed(:resolve_vulnerability_with_ai) } end - context 'when feature is not licensed' do - before do - stub_licensed_features(ai_features: false) - end + context 'when service is not authorized' do + let(:cloud_connector_user_access) { false } it { is_expected.to be_disallowed(:resolve_vulnerability_with_ai) } end - - describe 'cloud connector' do - using RSpec::Parameterized::TableSyntax - where(:free_access, :user_access, :allowed) do - true | true | true - true | false | true - false | true | true - false | false | false - end - - with_them do - let(:cloud_connector_free_access) { free_access } - let(:cloud_connector_user_access) { user_access } - let(:policy) { :resolve_vulnerability_with_ai } - - it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } - end - end - - context 'when on .org or .com', :saas do - using RSpec::Parameterized::TableSyntax - where(:group_with_ai_membership, :free_access, :user_access, :allowed) do - true | true | true | true - true | false | true | true - false | false | true | true - false | false | false | false - true | true | false | true - false | true | false | false - end - - with_them do - before do - allow(user).to receive(:any_group_with_ga_ai_available?).and_return(group_with_ai_membership) - end - - let(:cloud_connector_free_access) { free_access } - let(:cloud_connector_user_access) { user_access } - let(:policy) { :resolve_vulnerability_with_ai } - - it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } - end - end end context 'when feature is not authorized' do diff --git a/ee/spec/requests/api/code_suggestions_spec.rb b/ee/spec/requests/api/code_suggestions_spec.rb index 1b3fa40892902..d10bd3c9b9c13 100644 --- a/ee/spec/requests/api/code_suggestions_spec.rb +++ b/ee/spec/requests/api/code_suggestions_spec.rb @@ -146,7 +146,7 @@ def is_even(n: int) -> end let(:file_name) { 'test.py' } - + let(:service_name) { :code_suggestions } let(:additional_params) { {} } let(:body) do { @@ -209,9 +209,10 @@ def is_even(n: int) -> before do allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(0) - allow(::CloudConnector::AvailableServices).to receive(:find_by_name).and_return(service) - allow(service).to receive_messages({ free_access?: false, allowed_for?: true, access_token: token, + allow(::CloudConnector::AvailableServices).to receive(:find_by_name).with(service_name).and_return(service) + allow(service).to receive_messages({ access_token: token, name: service_name, enabled_by_namespace_ids: enabled_by_namespace_ids }) + allow(service).to receive_message_chain(:add_on_purchases, :assigned_to_user, :any?).and_return(true) stub_feature_flags(use_codestral_for_code_completions: false) end @@ -704,34 +705,6 @@ def get_user(session): expect(response).to have_gitlab_http_status(:bad_request) end end - - context 'when code suggestions feature is self hosted' do - let_it_be(:feature_setting) { create(:ai_feature_setting) } - - before do - allow(service).to receive_messages({ free_access?: true, allowed_for?: false, access_token: token }) - end - - context 'and requested before cut off date' do - it 'is unauthorized' do - post_api - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - context 'when self_hosted_models_beta_ended is disabled' do - before do - stub_feature_flags(self_hosted_models_beta_ended: false) - end - - it 'is unauthorized' do - post_api - - expect(response).to have_gitlab_http_status(:ok) - end - end - end - end end it_behaves_like 'code completions endpoint' @@ -768,6 +741,37 @@ def get_user(session): expect(params['Header']).not_to have_key('X-Gitlab-Saas-Namespace-Ids') expect(params['Header']).to include('X-Gitlab-Rails-Send-Start' => [Time.now.to_f.to_s]) end + + context 'when code suggestions feature is self hosted and requested before cut off date without seats' do + let_it_be(:feature_setting) { create(:ai_feature_setting, feature: :code_completions) } + + let(:service_name) { :self_hosted_models } + + before do + stub_licensed_features(ai_features: true) + + allow(service).to receive_messages(free_access?: true) + allow(service).to receive_message_chain(:add_on_purchases, :assigned_to_user, :any?).and_return(false) + end + + it 'is unauthorized' do + post_api + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + context 'when self_hosted_models_beta_ended is disabled' do + before do + stub_feature_flags(self_hosted_models_beta_ended: false) + end + + it 'is authorized' do + post_api + + expect(response).to have_gitlab_http_status(:ok) + end + end + end end it_behaves_like 'code completions endpoint' diff --git a/ee/spec/requests/api/graphql/mutations/projects/generate_commit_message_spec.rb b/ee/spec/requests/api/graphql/mutations/projects/generate_commit_message_spec.rb index b750c9894fa86..8aa48b320789a 100644 --- a/ee/spec/requests/api/graphql/mutations/projects/generate_commit_message_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/projects/generate_commit_message_spec.rb @@ -26,11 +26,8 @@ stub_licensed_features(generate_commit_message: true, ai_features: true, experimental_features: true) group.namespace_settings.update!(experiment_features_enabled: true) - service_data = CloudConnector::SelfManaged::AvailableServiceData.new(:generate_commit_message, nil, nil) - allow(CloudConnector::AvailableServices).to receive(:find_by_name) - .with(:generate_commit_message) - .and_return(service_data) - allow(service_data).to receive(:allowed_for?).with(current_user).and_return(true) + allow(current_user).to receive(:allowed_to_use?) + .with(:generate_commit_message, licensed_feature: :generate_commit_message).and_return(true) end it 'successfully performs an generate commit message request' do diff --git a/ee/spec/requests/api/graphql/user_code_suggestions_available_spec.rb b/ee/spec/requests/api/graphql/user_code_suggestions_available_spec.rb index a6a753346b5ff..0b02468ab167f 100644 --- a/ee/spec/requests/api/graphql/user_code_suggestions_available_spec.rb +++ b/ee/spec/requests/api/graphql/user_code_suggestions_available_spec.rb @@ -36,34 +36,38 @@ .to receive(:allowed?).and_call_original stub_licensed_features(code_suggestions: true) + + service = instance_double('::CloudConnector::SelfSigned::AvailableServiceData', name: :code_suggestions, + free_access?: false) + allow(::CloudConnector::AvailableServices).to receive(:find_by_name).and_return(service) + allow(service).to receive_message_chain(:add_on_purchases, :assigned_to_user, :any?) + .and_return(user_has_add_on_purchases) end context 'when user has access to code suggestions' do - it 'returns true' do - expect(CloudConnector::AvailableServices).to receive_message_chain(:find_by_name, - :allowed_for?).and_return(true) + let(:user_has_add_on_purchases) { true } + it 'returns true' do post_graphql(query, current_user: current_user) expect(graphql_response).to eq(true) end - end - context 'when user does not have access to code suggestions' do - it 'returns false' do - expect(CloudConnector::AvailableServices).to receive_message_chain(:find_by_name, - :allowed_for?).and_return(false) + context 'when feature flag is off' do + before do + stub_feature_flags(ai_duo_code_suggestions_switch: false) + end - post_graphql(query, current_user: current_user) + it 'returns false' do + post_graphql(query, current_user: current_user) - expect(graphql_response).to eq(false) + expect(graphql_response).to eq(false) + end end end - context 'when feature flag is off' do - before do - stub_feature_flags(ai_duo_code_suggestions_switch: false) - end + context 'when user does not have access to code suggestions' do + let(:user_has_add_on_purchases) { false } it 'returns false' do post_graphql(query, current_user: current_user) -- GitLab