diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 32ef830b6cbfbd577b710f730a32ffe41393283e..6fa76297679d9db44c186ffad032fe2f314c9bcb 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -39,6 +39,7 @@ = render_if_exists 'groups/settings/wiki', f: f, group: @group = render 'groups/settings/lfs', f: f = render_if_exists 'groups/settings/code_suggestions', f: f, group: @group + = render_if_exists 'groups/settings/ai_related_settings', f: f, group: @group = render 'groups/settings/git_access_protocols', f: f, group: @group = render 'groups/settings/project_creation_level', f: f, group: @group = render 'groups/settings/subgroup_creation_level', f: f, group: @group diff --git a/config/feature_flags/development/ai_related_settings.yml b/config/feature_flags/development/ai_related_settings.yml new file mode 100644 index 0000000000000000000000000000000000000000..3d4b24c69998bd493c28b8a4bd787f45f2dd8871 --- /dev/null +++ b/config/feature_flags/development/ai_related_settings.yml @@ -0,0 +1,8 @@ +--- +name: ai_related_settings +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118222 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/408791 +milestone: '16.0' +type: development +group: group::ai-enablement +default_enabled: false diff --git a/db/migrate/20230420115733_add_ai_settings_to_namespace_settings.rb b/db/migrate/20230420115733_add_ai_settings_to_namespace_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..681691d39c74ec20c2652fae592e51c26dcff88a --- /dev/null +++ b/db/migrate/20230420115733_add_ai_settings_to_namespace_settings.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddAiSettingsToNamespaceSettings < Gitlab::Database::Migration[2.1] + enable_lock_retries! + + def change + add_column :namespace_settings, :experiment_features_enabled, :boolean, default: false, null: false + add_column :namespace_settings, :third_party_ai_features_enabled, :boolean, default: true, null: false + end +end diff --git a/db/schema_migrations/20230420115733 b/db/schema_migrations/20230420115733 new file mode 100644 index 0000000000000000000000000000000000000000..cac0ef60f61ad87e42e5111175b1692f3017588e --- /dev/null +++ b/db/schema_migrations/20230420115733 @@ -0,0 +1 @@ +d59b8bdea46ede31ff3d66d5aa18f4efb3afc216b13392b27214d7b609695da8 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index d75c7822a92351fe0df735288b93e6f317b2a70c..2f878306f5d94ad7d49d149a5fefb7d40f70049f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -18788,6 +18788,8 @@ CREATE TABLE namespace_settings ( unique_project_download_limit_alertlist integer[] DEFAULT '{}'::integer[] NOT NULL, emails_enabled boolean DEFAULT true NOT NULL, code_suggestions boolean DEFAULT false NOT NULL, + experiment_features_enabled boolean DEFAULT false NOT NULL, + third_party_ai_features_enabled boolean DEFAULT true NOT NULL, CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255)), CONSTRAINT namespace_settings_unique_project_download_limit_alertlist_size CHECK ((cardinality(unique_project_download_limit_alertlist) <= 100)), CONSTRAINT namespace_settings_unique_project_download_limit_allowlist_size CHECK ((cardinality(unique_project_download_limit_allowlist) <= 100)) diff --git a/ee/app/controllers/ee/groups_controller.rb b/ee/app/controllers/ee/groups_controller.rb index 757ed6e5b00f7d818ebe8d8bdbce1449c3aca177..9ab843256c5ab569080a7feeb06be914a78df4a2 100644 --- a/ee/app/controllers/ee/groups_controller.rb +++ b/ee/app/controllers/ee/groups_controller.rb @@ -99,6 +99,10 @@ def group_params_ee params_ee << :prevent_forking_outside_group if can_change_prevent_forking?(current_user, current_group) params_ee << :code_suggestions if ai_assist_ui_enabled? + if experimental_and_third_party_ai_settings_enabled? + params_ee.push(:experiment_features_enabled, :third_party_ai_features_enabled) + end + if current_group&.feature_available?(:adjourned_deletion_for_projects_and_groups) && ::Feature.disabled?(:always_perform_delayed_deletion) params_ee << :delayed_project_removal @@ -112,8 +116,12 @@ def ai_assist_ui_enabled? ::Gitlab::CurrentSettings.should_check_namespace_plan? && ::Feature.enabled?(:ai_assist_ui) && ::Feature.enabled?(:ai_assist_flag, current_group) && - current_group.root? && - current_group.licensed_feature_available?(:ai_assist) + current_group.licensed_feature_available?(:ai_assist) && + current_group.root? + end + + def experimental_and_third_party_ai_settings_enabled? + current_group && current_group.ai_settings_allowed? end def current_group diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb index 4f66bfbeb80f1e9de8c0f13f3b9140782c76feb1..176760c9b67f085eb6e3a86beec1b4b0431f2864 100644 --- a/ee/app/models/ee/group.rb +++ b/ee/app/models/ee/group.rb @@ -62,9 +62,6 @@ module Group has_one :group_merge_request_approval_setting, inverse_of: :group has_one :deletion_schedule, class_name: 'GroupDeletionSchedule' - delegate :deleting_user, :marked_for_deletion_on, to: :deletion_schedule, allow_nil: true - delegate :repository_read_only, :code_suggestions, :code_suggestions=, to: :namespace_settings, allow_nil: true - has_one :group_wiki_repository has_many :repository_storage_moves, class_name: 'Groups::RepositoryStorageMove', inverse_of: :container @@ -74,6 +71,16 @@ module Group belongs_to :push_rule, inverse_of: :group + delegate :deleting_user, :marked_for_deletion_on, to: :deletion_schedule, allow_nil: true + + delegate :repository_read_only, :code_suggestions, :code_suggestions=, + :experiment_features_enabled, :experiment_features_enabled=, + :third_party_ai_features_enabled, :third_party_ai_features_enabled=, + to: :namespace_settings, allow_nil: true + + delegate :ai_settings_allowed?, + to: :namespace_settings + delegate :wiki_access_level=, to: :group_feature, allow_nil: true # Use +checked_file_template_project+ instead, which implements important diff --git a/ee/app/models/ee/namespace_setting.rb b/ee/app/models/ee/namespace_setting.rb index 561ecd5622ff56007cec33d4acd34bd9243c3e37..a1ae79428556ecb7abe062921340b2d6731a4538 100644 --- a/ee/app/models/ee/namespace_setting.rb +++ b/ee/app/models/ee/namespace_setting.rb @@ -21,8 +21,12 @@ module NamespaceSetting allow_nil: false, user_id_existence: true, if: :unique_project_download_limit_alertlist_changed? + validates :experiment_features_enabled, inclusion: { in: [true, false] } + validates :third_party_ai_features_enabled, inclusion: { in: [true, false] } validate :user_cap_allowed, if: -> { enabling_user_cap? } + validate :third_party_ai_settings_allowed + validate :experiment_features_allowed before_save :set_prevent_sharing_groups_outside_hierarchy, if: -> { user_cap_enabled? } after_save :disable_project_sharing!, if: -> { user_cap_enabled? } @@ -70,6 +74,14 @@ def unique_project_download_limit_alertlist self[:unique_project_download_limit_alertlist].presence || active_owner_ids end + def ai_settings_allowed? + ::Gitlab::CurrentSettings.should_check_namespace_plan? && + ::Feature.enabled?(:openai_experimentation) && + ::Feature.enabled?(:ai_related_settings, namespace) && + namespace.licensed_feature_available?(:ai_features) && + namespace.root? + end + private def enabling_user_cap? @@ -101,6 +113,20 @@ def active_owner_ids namespace.owners.active.pluck_primary_key end + + def third_party_ai_settings_allowed + return unless third_party_ai_features_enabled_changed? + return if ai_settings_allowed? + + errors.add(:third_party_ai_features_enabled, _('Third party AI settings not allowed.')) + end + + def experiment_features_allowed + return unless experiment_features_enabled_changed? + return if ai_settings_allowed? + + errors.add(:experiment_features_enabled, _("Experiment features' settings not allowed.")) + end end class_methods do diff --git a/ee/app/models/gitlab_subscriptions/features.rb b/ee/app/models/gitlab_subscriptions/features.rb index e5f6b2ac92190be16fd50b3ea6b4bc3561f318ab..6e5a99861a00fb7a54821e3248e53bc9032c87ea 100644 --- a/ee/app/models/gitlab_subscriptions/features.rb +++ b/ee/app/models/gitlab_subscriptions/features.rb @@ -175,6 +175,7 @@ class Features ].freeze ULTIMATE_FEATURES = %i[ + ai_features ai_tanuki_bot api_discovery api_fuzzing diff --git a/ee/app/views/groups/settings/_ai_related_settings.html.haml b/ee/app/views/groups/settings/_ai_related_settings.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..0c682964f27668ed53ab9eb81619faf63d368a72 --- /dev/null +++ b/ee/app/views/groups/settings/_ai_related_settings.html.haml @@ -0,0 +1,30 @@ +- return unless group.ai_settings_allowed? + +- docs_link_url = help_page_path('user/project/repository/code_suggestions') #('user/ai_features') +- docs_link_start = ''.html_safe % { url: docs_link_url } +- terms_link_start = ''.html_safe +- how_is_my_data_used = help_page_path('user/project/repository/code_suggestions') #('user/ai_features', anchor: 'data-usage') +- data_used_link_start = ''.html_safe % { url: how_is_my_data_used } + +%h5 + = s_('AI|Experiment features') + +%p + = s_('AI|These features could cause performance and stability issues and may change over time.') + = s_('AI| %{link_start}What are experiment features?%{link_end}').html_safe % { link_start: docs_link_start, link_end: ''.html_safe } + +.form-group.gl-mb-3 + = f.gitlab_ui_checkbox_component :experiment_features_enabled, + s_('AI|Use experiment features'), + help_text: s_('AI|Enabling these features is your acceptance of the %{link_start}GitLab Testing Agreement%{link_end}.').html_safe % { link_start: terms_link_start, link_end: ''.html_safe } + +%h5 + = s_('AI|Third-party AI services') + +%p + = s_('AI|Features that use third-party AI services require transmission of data, including personal data.') + = s_('AI| %{link_start}How is my data used?%{link_end}').html_safe % { link_start: data_used_link_start, link_end: ''.html_safe } + +.form-group.gl-mb-3 + = f.gitlab_ui_checkbox_component :third_party_ai_features_enabled, + s_('AI|Use third-party AI services') diff --git a/ee/config/audit_events/types/experiment_features_enabled_updated.yml b/ee/config/audit_events/types/experiment_features_enabled_updated.yml new file mode 100644 index 0000000000000000000000000000000000000000..9db501a515c55f6fd5fc4fd965a16ef49ecb366a --- /dev/null +++ b/ee/config/audit_events/types/experiment_features_enabled_updated.yml @@ -0,0 +1,9 @@ +--- +name: experiment_features_enabled_updated +description: Event triggered on toggling setting for enabling experiment AI features +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/404856/ +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118222 +feature_category: not_owned +milestone: '16.0' +saved_to_database: true +streamed: true diff --git a/ee/config/audit_events/types/third_party_ai_features_enabled_updated.yml b/ee/config/audit_events/types/third_party_ai_features_enabled_updated.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed1c527277c4a6b59f7885ef72dcd5e8a07b2e85 --- /dev/null +++ b/ee/config/audit_events/types/third_party_ai_features_enabled_updated.yml @@ -0,0 +1,9 @@ +--- +name: third_party_ai_features_enabled_updated +description: Event triggered on toggling setting for enabling third-party AI features +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/404856/ +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118222 +feature_category: not_owned +milestone: '16.0' +saved_to_database: true +streamed: true diff --git a/ee/lib/audit/namespace_setting_changes_auditor.rb b/ee/lib/audit/namespace_setting_changes_auditor.rb index 25e45bd0bb5a819818d797353977fbd5ef6193bb..678784076baf816908700231e2f8dbbbc9f51d6f 100644 --- a/ee/lib/audit/namespace_setting_changes_auditor.rb +++ b/ee/lib/audit/namespace_setting_changes_auditor.rb @@ -2,6 +2,12 @@ module Audit class NamespaceSettingChangesAuditor < BaseChangesAuditor + EVENT_NAME_PER_COLUMN = { + code_suggestions: 'code_suggestions_updated', + experiment_features_enabled: 'experiment_features_enabled_updated', + third_party_ai_features_enabled: 'third_party_ai_features_enabled_updated' + }.freeze + def initialize(current_user, namespace_setting, group) @group = group @@ -10,9 +16,10 @@ def initialize(current_user, namespace_setting, group) def execute return if model.blank? - return unless audit_required? :code_suggestions - audit_changes(:code_suggestions, entity: @group, model: model, event_type: 'code_suggestions_updated') + EVENT_NAME_PER_COLUMN.each do |column, event_name| + audit_changes(column, entity: @group, model: model, event_type: event_name) + end end private diff --git a/ee/spec/controllers/ee/groups_controller_spec.rb b/ee/spec/controllers/ee/groups_controller_spec.rb index 2f13976a2979aaa74819ca326c081fc473536955..bedd27f1d347ae7cf6466d398f938d96811eacb9 100644 --- a/ee/spec/controllers/ee/groups_controller_spec.rb +++ b/ee/spec/controllers/ee/groups_controller_spec.rb @@ -828,6 +828,40 @@ def request(visibility_level) end end + context 'when ai settings are specified' do + let(:group) { create(:group_with_plan, plan: :ultimate_plan, trial_ends_on: Date.tomorrow) } + + before do + allow(Gitlab).to receive(:com?).and_return(true) + stub_licensed_features(ai_features: true) + stub_ee_application_setting(should_check_namespace_plan: true) + group.add_owner(user) + + sign_in(user) + end + + it 'updates the attribute' do + put :update, params: { id: group.to_param, group: { experiment_features_enabled: true, + third_party_ai_features_enabled: false } } + + expect(group.reload.experiment_features_enabled).to eq(true) + expect(group.reload.third_party_ai_features_enabled).to eq(false) + end + + context 'when ai licensed features are not available for the group' do + before do + stub_licensed_features(ai_features: false) + end + + it 'does not update attributes' do + expect do + put :update, params: { id: group.to_param, group: { experiment_features_enabled: true, + third_party_ai_features_enabled: false } } + end.to not_change { group.reload.experiment_features_enabled }.and not_change { group.reload.third_party_ai_features_enabled } + end + end + end + describe '#ai_assist_ui_enabled?', :saas do let_it_be(:group) { create(:group_with_plan, plan: :ultimate_plan) } let_it_be(:subgroup) { create(:group, parent: group) } diff --git a/ee/spec/lib/audit/namespace_setting_changes_auditor_spec.rb b/ee/spec/lib/audit/namespace_setting_changes_auditor_spec.rb index 0985bcd0713e59987726b746463b861454040188..0c699679d5eb0d55a5bb7ea31ef06c76f3d34aa8 100644 --- a/ee/spec/lib/audit/namespace_setting_changes_auditor_spec.rb +++ b/ee/spec/lib/audit/namespace_setting_changes_auditor_spec.rb @@ -50,17 +50,103 @@ auditor.execute end end + + context 'when code_suggestions is not changed' do + before do + group.namespace_settings.update!(code_suggestions: true) + end + + it 'does not create an audit event' do + group.namespace_settings.update!(code_suggestions: true) + + expect { auditor.execute }.not_to change { AuditEvent.count } + end + end end - context 'when code_suggestions is not changed' do + context 'when ai-related settings are changed' do + let(:group) { create(:group_with_plan, plan: :ultimate_plan, trial_ends_on: Date.tomorrow) } + before do - group.namespace_settings.update!(code_suggestions: true) + allow(Gitlab).to receive(:com?).and_return(true) + stub_licensed_features(ai_features: true) + stub_ee_application_setting(should_check_namespace_plan: true) end - it 'does not create an audit event' do - group.namespace_settings.update!(code_suggestions: true) + context 'when experiment_features_enabled is changed' do + where(:prev_value, :new_value) do + true | false + false | true + end + + with_them do + before do + group.namespace_settings.update!(experiment_features_enabled: prev_value) + end + + it 'creates an audit event' do + group.namespace_settings.update!(experiment_features_enabled: new_value) + + expect { auditor.execute }.to change { AuditEvent.count }.by(1) + audit_details = { + change: :experiment_features_enabled, + from: prev_value, + to: new_value, + target_details: group.full_path + } + expect(AuditEvent.last.details).to include(audit_details) + end + end + end + + context 'when experiment_features_enabled is not changed' do + before do + group.namespace_settings.update!(experiment_features_enabled: true) + end - expect { auditor.execute }.not_to change { AuditEvent.count } + it 'does not create an audit event' do + group.namespace_settings.update!(experiment_features_enabled: true) + + expect { auditor.execute }.not_to change { AuditEvent.count } + end + end + + context 'when third_party_ai_features_enabled is changed' do + where(:prev_value, :new_value) do + true | false + false | true + end + + with_them do + before do + group.namespace_settings.update!(third_party_ai_features_enabled: prev_value) + end + + it 'creates an audit event' do + group.namespace_settings.update!(third_party_ai_features_enabled: new_value) + + expect { auditor.execute }.to change { AuditEvent.count }.by(1) + audit_details = { + change: :third_party_ai_features_enabled, + from: prev_value, + to: new_value, + target_details: group.full_path + } + expect(AuditEvent.last.details).to include(audit_details) + end + end + end + + context 'when third_party_ai_features_enabled is not changed' do + before do + group.namespace_settings.update!(third_party_ai_features_enabled: true) + end + + it 'does not create an audit event' do + group.namespace_settings.update!(third_party_ai_features_enabled: true) + + expect { auditor.execute }.not_to change { AuditEvent.count } + end end end end diff --git a/ee/spec/models/namespace_setting_spec.rb b/ee/spec/models/namespace_setting_spec.rb index d91a2ea63bef08a3d494fd07e26906aeb540ac25..51003a4e40eaed3bdd97aa0ad1b1a7d16ff4223f 100644 --- a/ee/spec/models/namespace_setting_spec.rb +++ b/ee/spec/models/namespace_setting_spec.rb @@ -44,6 +44,36 @@ expect(subject.errors[attr]).to include("exceeds maximum length (100 usernames)") end end + + describe 'AI related settings' do + subject(:settings) { group.namespace_settings } + + shared_examples 'AI related settings validations' do |attr| + before do + allow(subject).to receive(:ai_settings_allowed?).and_return(true) + end + + it { is_expected.to allow_value(false).for(attr) } + it { is_expected.to allow_value(true).for(attr) } + it { is_expected.not_to allow_value(nil).for(attr) } + + context 'when AI settings are not allowed' do + before do + allow(subject).to receive(:ai_settings_allowed?).and_return(false) + end + + it "#{attr} is not valid" do + subject[attr] = !subject[attr] + + expect(subject).not_to be_valid + expect(subject.errors[attr].first).to include("settings not allowed.") + end + end + end + + it_behaves_like 'AI related settings validations', :third_party_ai_features_enabled + it_behaves_like 'AI related settings validations', :experiment_features_enabled + end end describe 'unique_project_download_limit_alertlist', feature_category: :insider_threat do @@ -373,4 +403,32 @@ it_behaves_like '[configuration](inherit_group_setting: bool) and [configuration]_locked?', :only_allow_merge_if_all_discussions_are_resolved end end + + describe '.ai_settings_allowed?' do + using RSpec::Parameterized::TableSyntax + + where(:check_namespace_plan, :main_feature_flag, :secondary_feature_flag, :licensed_feature, :is_root, :result) do + true | true | true | true | true | true + false | true | true | true | true | false + true | false | true | true | true | false + true | true | false | true | true | false + true | true | true | false | true | false + true | true | true | true | false | false + end + + with_them do + let(:group) { create(:group) } + subject { group.namespace_settings.ai_settings_allowed? } + + before do + allow(Gitlab::CurrentSettings).to receive(:should_check_namespace_plan?).and_return(check_namespace_plan) + stub_feature_flags(openai_experimentation: main_feature_flag) + stub_feature_flags(ai_related_settings: secondary_feature_flag) + allow(group).to receive(:licensed_feature_available?).with(:ai_features).and_return(licensed_feature) + allow(group).to receive(:root?).and_return(is_root) + end + + it { is_expected.to eq result } + end + end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2b40c48772a9bdef82d279e4bf3ef904b22bb753..665c5192d492290a9a9fde98e3eeace3e6c563b2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1836,18 +1836,33 @@ msgstr "" msgid "AI actions" msgstr "" +msgid "AI| %{link_start}How is my data used?%{link_end}" +msgstr "" + +msgid "AI| %{link_start}What are experiment features?%{link_end}" +msgstr "" + msgid "AI|Close the Code Explanation" msgstr "" msgid "AI|Code Explanation" msgstr "" +msgid "AI|Enabling these features is your acceptance of the %{link_start}GitLab Testing Agreement%{link_end}." +msgstr "" + msgid "AI|Experiment" msgstr "" +msgid "AI|Experiment features" +msgstr "" + msgid "AI|Explain the code from %{filePath} in human understandable language presented in Markdown format. In the response add neither original code snippet nor any title. `%{text}`" msgstr "" +msgid "AI|Features that use third-party AI services require transmission of data, including personal data." +msgstr "" + msgid "AI|Helpful" msgstr "" @@ -1863,9 +1878,21 @@ msgstr "" msgid "AI|There is too much text in the chat. Please try again with a shorter text." msgstr "" +msgid "AI|These features could cause performance and stability issues and may change over time." +msgstr "" + +msgid "AI|Third-party AI services" +msgstr "" + msgid "AI|Unhelpful" msgstr "" +msgid "AI|Use experiment features" +msgstr "" + +msgid "AI|Use third-party AI services" +msgstr "" + msgid "AI|What does the selected code mean?" msgstr "" @@ -17571,6 +17598,9 @@ msgstr "" msgid "Experiment" msgstr "" +msgid "Experiment features' settings not allowed." +msgstr "" + msgid "Experiments" msgstr "" @@ -45134,6 +45164,9 @@ msgstr "" msgid "Third Party Advisory Link" msgstr "" +msgid "Third party AI settings not allowed." +msgstr "" + msgid "This %{issuableDisplayName} is locked. Only project members can comment." msgstr ""