diff --git a/app/models/project.rb b/app/models/project.rb index 58259816cc9a79d0a6c3613c50833eabe548c7ee..2f1fc9ae5a94ecfd608b1261d19bcfc6e187b91c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -561,6 +561,7 @@ def self.integration_association_name(name) delegate :enforce_auth_checks_on_uploads, :enforce_auth_checks_on_uploads= delegate :warn_about_potentially_unwanted_characters, :warn_about_potentially_unwanted_characters= delegate :code_suggestions, :code_suggestions= + delegate :duo_features_enabled, :duo_features_enabled= end end @@ -3246,11 +3247,6 @@ def instance_runner_running_jobs_count end strong_memoize_attr :instance_runner_running_jobs_count - def code_suggestions_enabled? - code_suggestions && (group.nil? || group.code_suggestions) - end - strong_memoize_attr :code_suggestions_enabled? - # Overridden in EE def allows_multiple_merge_request_assignees? false @@ -3266,6 +3262,11 @@ def on_demand_dast_available? false end + # Overridden in EE + def code_suggestions_enabled? + false + end + private # overridden in EE diff --git a/db/migrate/20240124212938_add_duo_features_enabled_to_project_settings.rb b/db/migrate/20240124212938_add_duo_features_enabled_to_project_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..9dd09e1434aa66d2aab1e868e0d7cfefa0f88f79 --- /dev/null +++ b/db/migrate/20240124212938_add_duo_features_enabled_to_project_settings.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddDuoFeaturesEnabledToProjectSettings < Gitlab::Database::Migration[2.2] + enable_lock_retries! + + milestone '16.9' + + def change + add_column :project_settings, :duo_features_enabled, :boolean, default: true, null: false + end +end diff --git a/db/schema_migrations/20240124212938 b/db/schema_migrations/20240124212938 new file mode 100644 index 0000000000000000000000000000000000000000..32e355dbeef6410546ead8f8ca583abd89f46e1a --- /dev/null +++ b/db/schema_migrations/20240124212938 @@ -0,0 +1 @@ +1ab3946da575910f8ae9ab220d1e1da61619b66a9ad09a7c2a90c2abda5056d9 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 1d51178479d5cbbd1d4cc5a462eced08dc94726d..4c3ee5b8cebb1cb14690b16bdf0947b1b6e483b4 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -22750,6 +22750,7 @@ CREATE TABLE project_settings ( pages_multiple_versions_enabled boolean DEFAULT false NOT NULL, allow_merge_without_pipeline boolean DEFAULT false NOT NULL, code_suggestions boolean DEFAULT true NOT NULL, + duo_features_enabled boolean DEFAULT true NOT NULL, CONSTRAINT check_1a30456322 CHECK ((char_length(pages_unique_domain) <= 63)), CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)), CONSTRAINT check_3ca5cbffe6 CHECK ((char_length(issue_branch_template) <= 255)), diff --git a/ee/app/models/ee/project.rb b/ee/app/models/ee/project.rb index 3ab5cba92fcab2013577f69d4710139660dbb60b..f8dca4e3d03cbadacfd148c9ee3b52ba489f2c9b 100644 --- a/ee/app/models/ee/project.rb +++ b/ee/app/models/ee/project.rb @@ -1232,12 +1232,31 @@ def on_demand_dast_available? ::Gitlab::FIPS.enabled? ? ::Feature.enabled?(:dast_ods_browser_based_scanner, self) : true end + override :code_suggestions_enabled? + def code_suggestions_enabled? + return super unless ::Gitlab.org_or_com? || ::License.feature_available?(:code_suggestions) + + if gitlab_com_and_feature_enabled? || self_managed_and_past_service_start_date? + duo_features_enabled + else + root_ancestor.code_suggestions + end + end + def gcp_artifact_registry_enabled? ::Feature.enabled?(:gcp_artifact_registry, self) && ::Gitlab::Saas.feature_available?(:google_artifact_registry) end private + def gitlab_com_and_feature_enabled? + ::Gitlab.org_or_com? && ::Feature.enabled?(:purchase_code_suggestions) + end + + def self_managed_and_past_service_start_date? + ::License.feature_available?(:code_suggestions) && ::CodeSuggestions::SelfManaged::SERVICE_START_DATE.past? + end + def latest_ingested_sbom_pipeline_id_redis_key "latest_ingested_sbom_pipeline_id/#{id}" end diff --git a/ee/spec/models/ee/project_spec.rb b/ee/spec/models/ee/project_spec.rb index f2a4de0fd1e43ee06018c3ac187a3eed5d5485b7..172ad5a49f206ad0d1191e3d3d785e066e265300 100644 --- a/ee/spec/models/ee/project_spec.rb +++ b/ee/spec/models/ee/project_spec.rb @@ -4426,5 +4426,84 @@ def stub_default_url_options(host) it { is_expected.to eq(false) } end + + describe '#code_suggestions_enabled?' do + let_it_be_with_reload(:project) { create(:project, :in_group) } + + context 'gitlab.com' do + where(:duo_features_enabled, :code_suggestions_enabled) do + true | true + false | false + end + + with_them do + context 'purchase_code_suggestions FF is enabled' do + before do + allow(::Gitlab).to receive(:org_or_com?).and_return(true) + project.project_setting.update!(duo_features_enabled: duo_features_enabled) + end + + it 'uses the duo_features_enabled project setting value' do + expect(project.code_suggestions_enabled?).to eq code_suggestions_enabled + end + end + end + + with_them do + context 'purchase_code_suggestions FF is not enabled' do + before do + allow(::Gitlab).to receive(:org_or_com?).and_return(true) + stub_feature_flags(purchase_code_suggestions: false) + project.root_ancestor.update!(code_suggestions: duo_features_enabled) + end + + it 'uses the legacy code_suggestions setting on the root group' do + expect(project.code_suggestions_enabled?).to eq code_suggestions_enabled + end + end + end + end + + context 'self-managed' do + where(:code_suggestions_license, :duo_features_enabled, :code_suggestions_enabled) do + true | true | true + true | false | false + false | true | false + false | false | false + end + + with_them do + context 'after service start date' do + before do + project.project_setting.update!(duo_features_enabled: duo_features_enabled) + allow(::Gitlab).to receive(:org_or_com?).and_return(false) + stub_licensed_features(code_suggestions: code_suggestions_license) + end + + it 'uses the duo_features_enabled project setting value' do + travel_to(::CodeSuggestions::SelfManaged::SERVICE_START_DATE + 1.day) do + expect(project.code_suggestions_enabled?).to eq code_suggestions_enabled + end + end + end + end + + with_them do + context 'before service start date' do + before do + project.root_ancestor.update!(code_suggestions: duo_features_enabled) + allow(::Gitlab).to receive(:org_or_com?).and_return(false) + stub_licensed_features(code_suggestions: code_suggestions_license) + end + + it 'uses the legacy code_suggestions setting on the root group' do + travel_to(::CodeSuggestions::SelfManaged::SERVICE_START_DATE - 1.day) do + expect(project.code_suggestions_enabled?).to eq code_suggestions_enabled + end + end + end + end + end + end end end diff --git a/ee/spec/models/namespace_setting_spec.rb b/ee/spec/models/namespace_setting_spec.rb index 445b1ed4e4fd00bdc7fbddc4fc12e366bb5a886b..c59c8d5e0b4f42729135fc58ad127c2afec068dc 100644 --- a/ee/spec/models/namespace_setting_spec.rb +++ b/ee/spec/models/namespace_setting_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe NamespaceSetting do +RSpec.describe NamespaceSetting, feature_category: :groups_and_projects, type: :model do let(:group) { create(:group) } let(:setting) { group.namespace_settings } diff --git a/ee/spec/requests/api/code_suggestions_spec.rb b/ee/spec/requests/api/code_suggestions_spec.rb index 7c035a4b4975b9f86e8110800631e8236a6bc837..16d16eaa032d1f35bd85cabf9c02435d1ec3ed59 100644 --- a/ee/spec/requests/api/code_suggestions_spec.rb +++ b/ee/spec/requests/api/code_suggestions_spec.rb @@ -813,27 +813,20 @@ def get_user(session): end end - context 'when checking in project has code suggestions enabled' do - let_it_be(:enabled_project) { create(:project, :with_code_suggestions_enabled) } - let(:current_user) { authorized_user } - let_it_be(:disabled_project) { create(:project, :with_code_suggestions_disabled) } - let_it_be(:secret_project) { create(:project, :with_code_suggestions_enabled) } + context 'when checking if project has duo features enabled' do + let_it_be(:enabled_project) { create(:project, :in_group, :private, :with_duo_features_enabled) } + let_it_be(:disabled_project) { create(:project, :in_group, :with_duo_features_disabled) } - before_all do - enabled_project.add_maintainer(authorized_user) - disabled_project.add_maintainer(authorized_user) - end + let(:current_user) { authorized_user } subject { post api("/code_suggestions/enabled", current_user), params: { project_path: project_path } } - context 'when not logged in' do - let(:current_user) { nil } - let(:project_path) { enabled_project.full_path } - - it { is_expected.to eq(401) } - end + context 'when authorized to view project' do + before_all do + enabled_project.add_maintainer(authorized_user) + disabled_project.add_maintainer(authorized_user) + end - context 'when authorized' do context 'when enabled' do let(:project_path) { enabled_project.full_path } @@ -845,18 +838,25 @@ def get_user(session): it { is_expected.to eq(403) } end + end - context 'when user cannot access project' do - let(:project_path) { secret_project.full_path } + context 'when not logged in' do + let(:current_user) { nil } + let(:project_path) { enabled_project.full_path } - it { is_expected.to eq(404) } - end + it { is_expected.to eq(401) } + end - context 'when does not exist' do - let(:project_path) { 'not_a_real_project' } + context 'when logged in but not authorized to view project' do + let(:project_path) { enabled_project.full_path } - it { is_expected.to eq(404) } - end + it { is_expected.to eq(404) } + end + + context 'when project for project path does not exist' do + let(:project_path) { 'not_a_real_project' } + + it { is_expected.to eq(404) } end end end diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb index fbde9164a4f5ea04685cc8935dce74ac830dfb6c..14e275a927e3d80ab1aec0029a608393c3823d49 100644 --- a/lib/api/entities/project.rb +++ b/lib/api/entities/project.rb @@ -41,7 +41,6 @@ class Project < BasicProjectDetails end end - expose :code_suggestions, documentation: { type: 'boolean' } expose :packages_enabled, documentation: { type: 'boolean' } expose :empty_repo?, as: :empty_repo, documentation: { type: 'boolean' } expose :archived?, as: :archived, documentation: { type: 'boolean' } diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index a2848bd025613647183c93009bc44237373121cf..e751a0d740320dd943187153acfbaf4a157c75e2 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -609,15 +609,15 @@ files { { 'README.md' => 'Hello World' } } end - trait :with_code_suggestions_enabled do + trait :with_duo_features_enabled do after(:create) do |project| - project.project_setting.update!(code_suggestions: true) + project.project_setting.update!(duo_features_enabled: true) end end - trait :with_code_suggestions_disabled do + trait :with_duo_features_disabled do after(:create) do |project| - project.project_setting.update!(code_suggestions: false) + project.project_setting.update!(duo_features_enabled: false) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e1924a57ad35693fb49de6c04297dddca67b3c85..cae5291fb56d147397d21b7c2c95836fe163ea6f 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -9195,6 +9195,14 @@ def create_hook it { is_expected.to be_falsy } end + describe '#code_suggestions_enabled?' do + let(:project) { build_stubbed(:project) } + + subject(:code_suggestions_enabled?) { project.code_suggestions_enabled? } + + it { is_expected.to be_falsy } + end + private def finish_job(export_job) diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index ada2a917c316945f857f7f4871a44e94dcf4e6a1..ff1aac2f6f75d4f4a6d93215dcdc50d9b2c660b2 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -180,6 +180,8 @@ project_setting: - encrypted_product_analytics_configurator_connection_string - encrypted_product_analytics_configurator_connection_string_iv - product_analytics_configurator_connection_string + - code_suggestions + - duo_features_enabled build_service_desk_setting: # service_desk_setting unexposed_attributes: