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