diff --git a/config/application_setting_columns/usage_billing.yml b/config/application_setting_columns/usage_billing.yml new file mode 100644 index 0000000000000000000000000000000000000000..bac043f03c1d7fcdcda7833d4b5de7dd60320f96 --- /dev/null +++ b/config/application_setting_columns/usage_billing.yml @@ -0,0 +1,13 @@ +--- +api_type: object +attr: usage_billing +clusterwide: true +column: usage_billing +db_type: jsonb +default: "'{}'::jsonb" +description: Usage Billing Settings. Check `ee/app/validators/json_schemas/usage_billing_settings.json` + for schema definition +encrypted: false +gitlab_com_different_than_default: false +jihu: false +not_null: true diff --git a/db/migrate/20251202120752_add_usage_billing_application_setting.rb b/db/migrate/20251202120752_add_usage_billing_application_setting.rb new file mode 100644 index 0000000000000000000000000000000000000000..6a6035d5a216885d0944d74fb92567eb6e499d32 --- /dev/null +++ b/db/migrate/20251202120752_add_usage_billing_application_setting.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddUsageBillingApplicationSetting < Gitlab::Database::Migration[2.3] + milestone '18.7' + + def change + add_column :application_settings, :usage_billing, :jsonb, default: {}, null: false + end +end diff --git a/db/migrate/20251202120837_add_usage_billing_namespace_setting.rb b/db/migrate/20251202120837_add_usage_billing_namespace_setting.rb new file mode 100644 index 0000000000000000000000000000000000000000..8ec73f662e666dc754d9583ee6516cd21476950a --- /dev/null +++ b/db/migrate/20251202120837_add_usage_billing_namespace_setting.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddUsageBillingNamespaceSetting < Gitlab::Database::Migration[2.3] + milestone '18.7' + + def change + add_column :namespace_settings, :usage_billing, :jsonb, default: {}, null: false + end +end diff --git a/db/migrate/20251202120931_add_usage_billing_hash_constraint_to_application_settings.rb b/db/migrate/20251202120931_add_usage_billing_hash_constraint_to_application_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..31a8fa5e3f2703a77bfdceff79099cd889f531d3 --- /dev/null +++ b/db/migrate/20251202120931_add_usage_billing_hash_constraint_to_application_settings.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddUsageBillingHashConstraintToApplicationSettings < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.7' + + CONSTRAINT_NAME = 'check_application_settings_usage_billing_is_hash' + + def up + add_check_constraint( + :application_settings, + "(jsonb_typeof(usage_billing) = 'object')", + CONSTRAINT_NAME + ) + end + + def down + remove_check_constraint :application_settings, CONSTRAINT_NAME + end +end diff --git a/db/migrate/20251202120957_add_usage_billing_hash_constraint_to_namespace_settings.rb b/db/migrate/20251202120957_add_usage_billing_hash_constraint_to_namespace_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..335aeeb5ec86a06c6fed6d78876c8e0b7e1ea996 --- /dev/null +++ b/db/migrate/20251202120957_add_usage_billing_hash_constraint_to_namespace_settings.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddUsageBillingHashConstraintToNamespaceSettings < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.7' + + CONSTRAINT_NAME = 'check_namespace_settings_usage_billing_is_hash' + + def up + add_check_constraint( + :namespace_settings, + "(jsonb_typeof(usage_billing) = 'object')", + CONSTRAINT_NAME + ) + end + + def down + remove_check_constraint :namespace_settings, CONSTRAINT_NAME + end +end diff --git a/db/schema_migrations/20251202120752 b/db/schema_migrations/20251202120752 new file mode 100644 index 0000000000000000000000000000000000000000..cb6e4534859a0f93a3345ade617573c27d2f924f --- /dev/null +++ b/db/schema_migrations/20251202120752 @@ -0,0 +1 @@ +e4de3d81fb9a25156abc39a78c5ed22cbd36c4b5b9d639dbe34436a5f57c2105 \ No newline at end of file diff --git a/db/schema_migrations/20251202120837 b/db/schema_migrations/20251202120837 new file mode 100644 index 0000000000000000000000000000000000000000..65673254b29a91763ed815b38d41f5176755d21c --- /dev/null +++ b/db/schema_migrations/20251202120837 @@ -0,0 +1 @@ +ccb776b0058751c517b6806afe11d2e563ce15e784931ba80f8ed365d846eb88 \ No newline at end of file diff --git a/db/schema_migrations/20251202120931 b/db/schema_migrations/20251202120931 new file mode 100644 index 0000000000000000000000000000000000000000..0317ceff34245c114019c3f0c61f00f67a7e2e61 --- /dev/null +++ b/db/schema_migrations/20251202120931 @@ -0,0 +1 @@ +fc85115f5ced481a7a2843050c498145d5e725c75d7251ed3f0a9adcea077593 \ No newline at end of file diff --git a/db/schema_migrations/20251202120957 b/db/schema_migrations/20251202120957 new file mode 100644 index 0000000000000000000000000000000000000000..384c81e1ccb743df7d2d643931c6ab1537b01d7a --- /dev/null +++ b/db/schema_migrations/20251202120957 @@ -0,0 +1 @@ +bd0d5b5dbcdb1496e2e80b8badf17dce4f539807ff1d0e818bf635fed15b0538 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index ed9d388320b36cab618dbb3e3d772e6d606ffb23..0807e60d9211eab92e7f304d858394abba95341c 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -12471,6 +12471,7 @@ CREATE TABLE application_settings ( iframe_rendering_enabled boolean DEFAULT false NOT NULL, iframe_rendering_allowlist text, database_settings jsonb DEFAULT '{}'::jsonb NOT NULL, + usage_billing jsonb DEFAULT '{}'::jsonb NOT NULL, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)), CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)), @@ -12544,6 +12545,7 @@ CREATE TABLE application_settings ( CONSTRAINT check_application_settings_sign_in_restrictions_is_hash CHECK ((jsonb_typeof(sign_in_restrictions) = 'object'::text)), CONSTRAINT check_application_settings_token_prefixes_is_hash CHECK ((jsonb_typeof(token_prefixes) = 'object'::text)), CONSTRAINT check_application_settings_transactional_emails_is_hash CHECK ((jsonb_typeof(transactional_emails) = 'object'::text)), + CONSTRAINT check_application_settings_usage_billing_is_hash CHECK ((jsonb_typeof(usage_billing) = 'object'::text)), CONSTRAINT check_application_settings_vscode_extension_marketplace_is_hash CHECK ((jsonb_typeof(vscode_extension_marketplace) = 'object'::text)), CONSTRAINT check_b8c74ea5b3 CHECK ((char_length(deactivation_email_additional_text) <= 1000)), CONSTRAINT check_babd774f3c CHECK ((char_length(secret_detection_service_url) <= 255)), @@ -21656,9 +21658,11 @@ CREATE TABLE namespace_settings ( lock_duo_foundational_flows_enabled boolean DEFAULT false NOT NULL, duo_sast_fp_detection_enabled boolean, lock_duo_sast_fp_detection_enabled boolean DEFAULT false NOT NULL, + usage_billing jsonb DEFAULT '{}'::jsonb NOT NULL, CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255)), CONSTRAINT check_d9644d516f CHECK ((char_length(step_up_auth_required_oauth_provider) <= 255)), CONSTRAINT check_namespace_settings_security_policies_is_hash CHECK ((jsonb_typeof(security_policies) = 'object'::text)), + CONSTRAINT check_namespace_settings_usage_billing_is_hash CHECK ((jsonb_typeof(usage_billing) = 'object'::text)), 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/doc/api/settings.md b/doc/api/settings.md index 7f25b302f01167d80308501d4a287a9be4d15f40..0a755fe104d5ec3d86b0041a6d19c64f16a0621a 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -844,6 +844,7 @@ to configure other related settings. These requirements are | `iframe_rendering_enabled` | boolean | no | Allow rendering of iframes in Markdown. Disabled by default. | | `iframe_rendering_allowlist` | array of strings | no | List of allowed iframe `src` host[:port] entries used for Content Security Policy and sanitization. | | `iframe_rendering_allowlist_raw` | string | no | Raw newline- or comma-separated list of allowed iframe `src` host[:port] entries. | +| `usage_billing` | object | no | Usage Billing Settings. Check `ee/app/validators/json_schemas/usage_billing_settings.json` for schema definition | ### Dormant project settings diff --git a/doc/development/cells/application_settings_analysis.md b/doc/development/cells/application_settings_analysis.md index e2c7643c11847dde5c749008f6dc4f3023ed0f7b..a7e7d0b2267a30007a1bbad64fcd22608d777fb2 100644 --- a/doc/development/cells/application_settings_analysis.md +++ b/doc/development/cells/application_settings_analysis.md @@ -14,12 +14,12 @@ title: Application Settings analysis ## Statistics -- Number of attributes: 508 +- Number of attributes: 510 - Number of encrypted attributes: 42 (8.0%) -- Number of attributes documented: 297 (57.99999999999999%) +- Number of attributes documented: 298 (57.99999999999999%) - Number of attributes on GitLab.com different from the defaults: 224 (44.0%) -- Number of attributes with `clusterwide` set: 508 (100.0%) -- Number of attributes with `clusterwide: true` set: 135 (27.0%) +- Number of attributes with `clusterwide` set: 510 (100.0%) +- Number of attributes with `clusterwide: true` set: 137 (27.0%) ## Individual columns @@ -507,6 +507,7 @@ title: Application Settings analysis | `update_runner_versions_enabled` | `false` | `boolean` | `boolean` | `true` | `true` | `false` | `false`| `true` | | `updated_at` | `false` | `timestamp` | `` | `false` | `null` | `true` | `false`| `false` | | `updating_name_disabled_for_users` | `false` | `boolean` | `boolean` | `true` | `false` | `false` | `false`| `true` | +| `usage_billing` | `false` | `jsonb` | `object` | `true` | `'{}'::jsonb` | `false` | `true`| `true` | | `usage_ping_enabled` | `false` | `boolean` | `boolean` | `true` | `true` | `false` | `false`| `true` | | `usage_ping_features_enabled` | `false` | `boolean` | `` | `true` | `false` | `false` | `false`| `false` | | `usage_ping_generation_enabled` | `false` | `boolean` | `` | `true` | `true` | `false` | `false`| `false` | diff --git a/ee/app/models/ee/application_setting.rb b/ee/app/models/ee/application_setting.rb index 2fc8a20c7721920b46ade5bfd89a8be0cd66f6cf..afe9cf8a2cb717ea811866798407d7b146273882 100644 --- a/ee/app/models/ee/application_setting.rb +++ b/ee/app/models/ee/application_setting.rb @@ -66,6 +66,11 @@ module ApplicationSetting message: "must be one of: #{Ai::Conversation::Thread::EXPIRATION_COLUMNS.join(', ')}" } + jsonb_accessor :usage_billing, + display_gitlab_credits_user_data: [:boolean, { default: false }] + + validates :usage_billing, json_schema: { filename: "usage_billing_settings" } + jsonb_accessor :integrations, allow_all_integrations: [:boolean, { default: true }], allowed_integrations: [:string, { array: true, default: [] }] @@ -264,8 +269,6 @@ module ApplicationSetting :virtual_registries_endpoints_api_limit, numericality: { only_integer: true, greater_than_or_equal_to: 0 } - validates :dashboard_limit_enabled, inclusion: { in: [true, false], message: 'must be a boolean value' } - validates :cube_api_base_url, length: { maximum: 512 }, addressable_url: ::ApplicationSetting::ADDRESSABLE_URL_VALIDATION_OPTIONS.merge({ allow_localhost: true }), @@ -287,9 +290,6 @@ module ApplicationSetting presence: true, if: ->(setting) { setting.product_analytics_enabled } - validates :security_policy_global_group_approvers_enabled, - inclusion: { in: [true, false], message: 'must be a boolean value' } - validates :security_approval_policies_limit, numericality: { only_integer: true, @@ -325,9 +325,6 @@ module ApplicationSetting validates :package_metadata_purl_types, inclusion: { in: ::Enums::Sbom.purl_types.values } - validates :allow_account_deletion, - inclusion: { in: [true, false], message: N_('must be a boolean value') } - validates :delete_unconfirmed_users, inclusion: { in: [true, false], message: N_('must be a boolean value') }, unless: :email_confirmation_setting_off? @@ -348,10 +345,6 @@ module ApplicationSetting inclusion: { in: [true, false], message: N_('must be a boolean value') }, if: :gitlab_dedicated_instance - validates :instance_level_ai_beta_features_enabled, - allow_nil: false, - inclusion: { in: [true, false], message: N_('must be a boolean value') } - validates :zoekt_settings, json_schema: { filename: 'application_setting_zoekt_settings' } validates :zoekt_cpu_to_tasks_ratio, numericality: { greater_than: 0.0 } validates :zoekt_indexing_parallelism, numericality: { greater_than: 0 } @@ -377,13 +370,20 @@ module ApplicationSetting validates :code_creation, json_schema: { filename: 'application_setting_code_creation' } - validates :observability_backend_ssl_verification_enabled, - allow_nil: false, - inclusion: { in: [true, false], message: N_('must be a boolean value') } - - validates :auto_duo_code_review_enabled, :duo_remote_flows_enabled, - :duo_foundational_flows_enabled, :duo_sast_fp_detection_enabled, - inclusion: { in: [true, false] } + with_options(inclusion: { in: [true, false], message: N_('must be a boolean value') }) do + validates( + :dashboard_limit_enabled, + :security_policy_global_group_approvers_enabled, + :allow_account_deletion, + :instance_level_ai_beta_features_enabled, + :observability_backend_ssl_verification_enabled, + :auto_duo_code_review_enabled, + :duo_remote_flows_enabled, + :duo_foundational_flows_enabled, + :duo_sast_fp_detection_enabled, + :display_gitlab_credits_user_data + ) + end validate :duo_settings_immutable_on_saas, on: :update, diff --git a/ee/app/models/ee/namespace_setting.rb b/ee/app/models/ee/namespace_setting.rb index c47e1fdcd6a2f29d2ffa7bab51d19f0858568afe..0bb14076ddf0abe084fb5f2e456ab3569b3a6435 100644 --- a/ee/app/models/ee/namespace_setting.rb +++ b/ee/app/models/ee/namespace_setting.rb @@ -41,7 +41,6 @@ module NamespaceSetting allow_nil: false, user_id_existence: true, if: :unique_project_download_limit_alertlist_changed? - validates :experiment_features_enabled, inclusion: { in: [true, false] } alias_attribute :duo_core_features_enabled, :duo_nano_features_enabled @@ -84,6 +83,18 @@ module NamespaceSetting only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 20, allow_nil: true } + with_options(inclusion: { in: [true, false], message: N_('must be a boolean value') }) do + validates( + :experiment_features_enabled, + :display_gitlab_credits_user_data + ) + end + + jsonb_accessor :usage_billing, + display_gitlab_credits_user_data: [:boolean, { default: false }] + + validates :usage_billing, json_schema: { filename: "usage_billing_settings" } + jsonb_accessor :security_policies, pipeline_execution_policies_per_configuration_limit: [:integer, { default: 0 }] jsonb_accessor :security_policies, scan_execution_policies_per_configuration_limit: [:integer, { default: 0 }] jsonb_accessor :security_policies, approval_policies_per_configuration_limit: [:integer, { default: 0 }] diff --git a/ee/app/validators/json_schemas/usage_billing_settings.json b/ee/app/validators/json_schemas/usage_billing_settings.json new file mode 100644 index 0000000000000000000000000000000000000000..ea617ab53c736903291bbced9bd2c684954895e4 --- /dev/null +++ b/ee/app/validators/json_schemas/usage_billing_settings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Usage Billing settings", + "type": "object", + "additionalProperties": false, + "properties": { + "display_gitlab_credits_user_data": { + "type": "boolean", + "description": "Enable display of user data in GitLab Credits dashboards" + } + } +} diff --git a/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb b/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb index 1e54928522a3042d2976285e21214039e1682655..dea1f4d160de540ecf8e2ea6d0caf6ae6b7afdee 100644 --- a/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb +++ b/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb @@ -130,7 +130,8 @@ web_based_commit_signing_enabled allow_enterprise_bypass_placeholder_confirmation enterprise_bypass_expires_at allow_personal_snippets lock_auto_duo_code_review_enabled auto_duo_code_review_enabled lock_duo_remote_flows_enabled duo_remote_flows_enabled lock_duo_foundational_flows_enabled - duo_foundational_flows_enabled lock_duo_sast_fp_detection_enabled duo_sast_fp_detection_enabled] + duo_foundational_flows_enabled lock_duo_sast_fp_detection_enabled duo_sast_fp_detection_enabled + usage_billing] columns_to_audit = Namespaces::NamespaceSettingChangesAuditor::EVENT_NAME_PER_COLUMN.keys.map(&:to_s) diff --git a/ee/spec/models/application_setting_spec.rb b/ee/spec/models/application_setting_spec.rb index 2a08c933d98d5c3f7bdb1fe2c2fb9a601d4de696..851f777fbe4eeca9288c77a1b77ce9a8258e76fb 100644 --- a/ee/spec/models/application_setting_spec.rb +++ b/ee/spec/models/application_setting_spec.rb @@ -172,7 +172,8 @@ zoekt_rollout_retry_interval: Search::Zoekt::Settings::DEFAULT_ROLLOUT_RETRY_INTERVAL, zoekt_lost_node_threshold: Search::Zoekt::Settings::DEFAULT_LOST_NODE_THRESHOLD, zoekt_search_enabled: false, - enforce_pipl_compliance: false + enforce_pipl_compliance: false, + display_gitlab_credits_user_data: false ) end end @@ -1454,6 +1455,13 @@ it { is_expected.to allow_value(['project'] * 1000).for(:ci_cd_catalog_projects_allowlist) } it { is_expected.not_to allow_value(['project'] * 1001).for(:ci_cd_catalog_projects_allowlist) } end + + describe 'display_gitlab_credits_user_data' do + it 'validates display_gitlab_credits_user_data' do + expect(setting).to validate_inclusion_of(:display_gitlab_credits_user_data) + .in_array([true, false]) + end + end end describe 'search curation settings after .create_from_defaults', feature_category: :global_search do diff --git a/ee/spec/models/namespace_setting_spec.rb b/ee/spec/models/namespace_setting_spec.rb index 55059eee021c49f7f8c12686b4b673b68c3cfb2a..899751ebafee73c88e885f8639c1e3cd3fb603cc 100644 --- a/ee/spec/models/namespace_setting_spec.rb +++ b/ee/spec/models/namespace_setting_spec.rb @@ -63,8 +63,6 @@ describe 'experiment features' do let(:attr) { :experiment_features_enabled } - subject(:settings) { group.namespace_settings } - before do allow(subject).to receive(:experiment_settings_allowed?).and_return(true) end @@ -295,6 +293,13 @@ .is_less_than_or_equal_to(1827) end end + + describe 'display_gitlab_credits_user_data' do + it 'validates display_gitlab_credits_user_data' do + expect(settings).to validate_inclusion_of(:display_gitlab_credits_user_data) + .in_array([true, false]) + end + end end describe 'after_commit' do