diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb index 504daf2662e12a278589a225236add6521a4ca55..595e34821af7d13a9eaacbf67483253701ed1daa 100644 --- a/app/models/namespace_setting.rb +++ b/app/models/namespace_setting.rb @@ -24,14 +24,27 @@ class NamespaceSetting < ApplicationRecord chronic_duration_attr :subgroup_runner_token_expiration_interval_human_readable, :subgroup_runner_token_expiration_interval chronic_duration_attr :project_runner_token_expiration_interval_human_readable, :project_runner_token_expiration_interval - NAMESPACE_SETTINGS_PARAMS = [:default_branch_name, :delayed_project_removal, - :lock_delayed_project_removal, :resource_access_token_creation_allowed, - :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap, - :setup_for_company, :jobs_to_be_done, :runner_token_expiration_interval, :enabled_git_access_protocol, - :subgroup_runner_token_expiration_interval, :project_runner_token_expiration_interval].freeze + NAMESPACE_SETTINGS_PARAMS = %i[ + default_branch_name + delayed_project_removal + lock_delayed_project_removal + resource_access_token_creation_allowed + prevent_sharing_groups_outside_hierarchy + new_user_signups_cap + setup_for_company + jobs_to_be_done + runner_token_expiration_interval + enabled_git_access_protocol + subgroup_runner_token_expiration_interval + project_runner_token_expiration_interval + ].freeze self.primary_key = :namespace_id + def self.allowed_namespace_settings_params + NAMESPACE_SETTINGS_PARAMS + end + sanitizes! :default_branch_name def prevent_sharing_groups_outside_hierarchy diff --git a/app/services/groups/base_service.rb b/app/services/groups/base_service.rb index 06136aff50ec1dde58e339b3ded8ccd4cbbbf15e..9705f3a560d9228dc0e24ac37cb46f2ed4a544f4 100644 --- a/app/services/groups/base_service.rb +++ b/app/services/groups/base_service.rb @@ -13,11 +13,11 @@ def initialize(group, user, params = {}) private def handle_namespace_settings - settings_params = params.slice(*::NamespaceSetting::NAMESPACE_SETTINGS_PARAMS) + settings_params = params.slice(*::NamespaceSetting.allowed_namespace_settings_params) return if settings_params.empty? - ::NamespaceSetting::NAMESPACE_SETTINGS_PARAMS.each do |nsp| + ::NamespaceSetting.allowed_namespace_settings_params.each do |nsp| params.delete(nsp) end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 639f7c68c40c71a1273a3b74a03b7b439f9ae696..35716f7742a82cdf43069e933f80b20b3b2ae435 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -13,7 +13,7 @@ def execute remove_unallowed_params set_visibility_level - @group = Group.new(params.except(*::NamespaceSetting::NAMESPACE_SETTINGS_PARAMS)) + @group = Group.new(params.except(*::NamespaceSetting.allowed_namespace_settings_params)) @group.build_namespace_settings handle_namespace_settings diff --git a/config/locales/en.yml b/config/locales/en.yml index 233dca33bb88a5e6fdd6e6fea7d592242924b0e7..56df8f93113a48b9429693191da2d8816619614e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -10,6 +10,9 @@ en: target: Target issue group: path: Group URL + namespace_setting: + unique_project_download_limit: "Number of projects" + unique_project_download_limit_interval_in_seconds: "Interval (seconds)" member: user: "The member's email address" invite_email: "The member's email address" diff --git a/db/migrate/20220613054349_add_unique_project_download_limit_settings_to_namespace_settings.rb b/db/migrate/20220613054349_add_unique_project_download_limit_settings_to_namespace_settings.rb new file mode 100644 index 0000000000000000000000000000000000000000..7e821cb17a2f6d6421a2a8dd855637e58488e7cc --- /dev/null +++ b/db/migrate/20220613054349_add_unique_project_download_limit_settings_to_namespace_settings.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AddUniqueProjectDownloadLimitSettingsToNamespaceSettings < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + def change + add_column :namespace_settings, :unique_project_download_limit, :smallint, + default: 0, null: false + add_column :namespace_settings, :unique_project_download_limit_interval_in_seconds, :integer, + default: 0, null: false + end +end diff --git a/db/schema_migrations/20220613054349 b/db/schema_migrations/20220613054349 new file mode 100644 index 0000000000000000000000000000000000000000..1c3806a80c8b885d84907187fdf045003b3f4263 --- /dev/null +++ b/db/schema_migrations/20220613054349 @@ -0,0 +1 @@ +4c3e4852614dd1a59d63809c40417887794bcbbcf8d3ea3a96f8846e2bd5f795 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index f697bacdfc51ac46f902b9b7d14af64b2d28816b..2013a434bab1e63651653b9bdf6acff85c431722 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17550,6 +17550,8 @@ CREATE TABLE namespace_settings ( project_runner_token_expiration_interval integer, exclude_from_free_user_cap boolean DEFAULT false NOT NULL, enabled_git_access_protocol smallint DEFAULT 0 NOT NULL, + unique_project_download_limit smallint DEFAULT 0 NOT NULL, + unique_project_download_limit_interval_in_seconds integer DEFAULT 0 NOT NULL, CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255)) ); diff --git a/ee/app/controllers/groups/settings/reporting_controller.rb b/ee/app/controllers/groups/settings/reporting_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..705c4067deeadc8b12c7926d08e2533d551982a9 --- /dev/null +++ b/ee/app/controllers/groups/settings/reporting_controller.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Groups + module Settings + class ReportingController < Groups::ApplicationController + layout 'group_settings' + + before_action :check_feature_availability + before_action :authorize_admin_group! + + feature_category :insider_threat + urgency :low + + def show + end + + def update + if Groups::UpdateService.new(@group, current_user, group_params).execute + notice = _('Group "%{group_name}" was successfully updated.' % { group_name: @group.name }) + + redirect_to group_settings_reporting_path(@group), notice: notice + else + render action: "show" + end + end + + private + + def group_params + params.require(:group).permit(%i[ + unique_project_download_limit + unique_project_download_limit_interval_in_seconds + ]) + end + + def check_feature_availability + render_404 unless group.unique_project_download_limit_enabled? + end + end + end +end diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb index 9d72dda8d51cbc49ba80d42fb6d3771a6da6319c..53cd6da39bb6ddb78ddfe45ff0449579d1ba1566 100644 --- a/ee/app/models/ee/group.rb +++ b/ee/app/models/ee/group.rb @@ -177,6 +177,12 @@ def enforced_sso? def repository_read_only? !!namespace_settings&.repository_read_only? end + + def unique_project_download_limit_enabled? + root? && + ::Feature.enabled?(:limit_unique_project_downloads_per_namespace_user, self) && + licensed_feature_available?(:unique_project_download_limit) + end end class_methods do diff --git a/ee/app/models/ee/namespace_setting.rb b/ee/app/models/ee/namespace_setting.rb index 92d63050df5e590cddc981560241ddd8b763069b..d63e58c88106b1219280f0c82031cc14d7a662bf 100644 --- a/ee/app/models/ee/namespace_setting.rb +++ b/ee/app/models/ee/namespace_setting.rb @@ -5,6 +5,13 @@ module NamespaceSetting extend ActiveSupport::Concern prepended do + validates :unique_project_download_limit, + numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 10_000 }, + presence: true + validates :unique_project_download_limit_interval_in_seconds, + numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 10.days.to_i }, + presence: true + validate :user_cap_allowed, if: -> { enabling_user_cap? } before_save :set_prevent_sharing_groups_outside_hierarchy, if: -> { user_cap_enabled? } @@ -46,5 +53,19 @@ def user_cap_enabled? new_user_signups_cap.present? && namespace.root? end end + + class_methods do + extend ::Gitlab::Utils::Override + + EE_NAMESPACE_SETTINGS_PARAMS = %i[ + unique_project_download_limit + unique_project_download_limit_interval_in_seconds + ].freeze + + override :allowed_namespace_settings_params + def allowed_namespace_settings_params + super + EE_NAMESPACE_SETTINGS_PARAMS + end + end end end diff --git a/ee/app/models/gitlab_subscriptions/features.rb b/ee/app/models/gitlab_subscriptions/features.rb index 5f520d44559e395ce2ffd16e7ad9a988ec2f9ee9..f404571964d5a217d7f90e3d7910dd8003e5a872 100644 --- a/ee/app/models/gitlab_subscriptions/features.rb +++ b/ee/app/models/gitlab_subscriptions/features.rb @@ -216,6 +216,7 @@ class Features stale_runner_cleanup_for_namespace status_page subepics + unique_project_download_limit vulnerability_auto_fix vulnerability_finding_signatures ].freeze diff --git a/ee/app/views/groups/settings/reporting/show.html.haml b/ee/app/views/groups/settings/reporting/show.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..2e207d24c642ad4ecce56ede883042b4f94a485b --- /dev/null +++ b/ee/app/views/groups/settings/reporting/show.html.haml @@ -0,0 +1,25 @@ +- title = s_("GroupSettings|Reporting") +- breadcrumb_title title +- page_title title + +%h4.settings-title + = s_('GroupSettings|Project download rate limit') +%p + = s_('GroupSettings|Automatically ban users who download more than the specified number of projects within the specified interval.') + += form_errors(@group.namespace_settings) + += gitlab_ui_form_for @group, url: group_settings_reporting_path(@group) do |f| + .form-group + = f.label :unique_project_download_limit, s_('GroupSettings|Number of projects'), class: 'label-bold' + = f.number_field :unique_project_download_limit, value: @group.namespace_settings&.unique_project_download_limit, class: 'form-control gl-form-input' + .form-text.text-muted + = s_("GroupSettings|The maximum number of unique projects a user can download within the specified interval before they're banned. Set to 0 to disable limiting.") + + .form-group + = f.label :unique_project_download_limit_interval_in_seconds, s_('GroupSettings|Interval (seconds)'), class: 'label-bold' + = f.number_field :unique_project_download_limit_interval_in_seconds, value: @group.namespace_settings&.unique_project_download_limit_interval_in_seconds, class: 'form-control gl-form-input' + .form-text.text-muted + = s_('GroupSettings|Set to 0 to disable limiting.') + + = f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mt-4' diff --git a/ee/config/feature_flags/development/limit_unique_project_downloads_per_namespace_user.yml b/ee/config/feature_flags/development/limit_unique_project_downloads_per_namespace_user.yml new file mode 100644 index 0000000000000000000000000000000000000000..9e2735672fb06102d15ba9d24278370477ea80f6 --- /dev/null +++ b/ee/config/feature_flags/development/limit_unique_project_downloads_per_namespace_user.yml @@ -0,0 +1,8 @@ +--- +name: limit_unique_project_downloads_per_namespace_user +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89996 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/365724 +milestone: '15.2' +type: development +group: group::anti-abuse +default_enabled: false diff --git a/ee/config/routes/group.rb b/ee/config/routes/group.rb index 3487f2a64b38fd88999831029df95218ede82192..20aef35be02de661e4a7a7c97c35846cb03c04a2 100644 --- a/ee/config/routes/group.rb +++ b/ee/config/routes/group.rb @@ -7,6 +7,10 @@ constraints: { group_id: Gitlab::PathRegex.full_namespace_route_regex }) do draw :wiki + namespace :settings do + resource :reporting, only: [:show, :update], controller: 'reporting' + end + resources :group_members, only: [], concerns: :access_requestable do patch :override, on: :member diff --git a/ee/lib/ee/sidebars/groups/menus/settings_menu.rb b/ee/lib/ee/sidebars/groups/menus/settings_menu.rb index 3b59a60eed7d0689d20981c50055e8f60ac11701..347f5aefb3bb4b759f4593b1157341dad88c8ec0 100644 --- a/ee/lib/ee/sidebars/groups/menus/settings_menu.rb +++ b/ee/lib/ee/sidebars/groups/menus/settings_menu.rb @@ -18,6 +18,7 @@ def configure_menu_items add_item(saml_group_links_menu_item) add_item(usage_quotas_menu_item) add_item(billing_menu_item) + add_item(reporting_menu_item) true end @@ -133,6 +134,19 @@ def administration_nav_item_disabled? ::Feature.disabled?(:group_administration_nav_item, context.group) end end + + def reporting_menu_item + unless context.group.unique_project_download_limit_enabled? + return ::Sidebars::NilMenuItem.new(item_id: :reporting) + end + + ::Sidebars::MenuItem.new( + title: s_('GroupSettings|Reporting'), + link: group_settings_reporting_path(context.group), + active_routes: { path: 'reporting#show' }, + item_id: :reporting + ) + end end end end diff --git a/ee/spec/features/groups/settings/reporting_spec.rb b/ee/spec/features/groups/settings/reporting_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..36009d0cd2a200222f6585381d44c887d72d1843 --- /dev/null +++ b/ee/spec/features/groups/settings/reporting_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Group reporting settings' do + let_it_be(:user) { create(:user) } + + let(:group) { create(:group) } + let(:feature_flag_enabled) { true } + let(:licensed_feature_available) { true } + let(:current_limit) { 1 } + let(:current_interval) { 9 } + + before do + stub_feature_flags(limit_unique_project_downloads_per_namespace_user: feature_flag_enabled) + stub_licensed_features(unique_project_download_limit: licensed_feature_available) + + sign_in(user) + + group.add_owner(user) + + group.namespace_settings.update!( + unique_project_download_limit: current_limit, + unique_project_download_limit_interval_in_seconds: current_interval + ) + + visit group_settings_reporting_path(group) + end + + it 'displays the side bar menu item' do + page.within('.shortcuts-settings') do + expect(page).to have_link 'Reporting', href: group_settings_reporting_path(group) + end + end + + it 'updates the settings' do + limit_label = s_('GroupSettings|Number of projects') + interval_label = s_('GroupSettings|Interval (seconds)') + + expect(page).to have_field(limit_label, with: current_limit) + expect(page).to have_field(interval_label, with: current_interval) + + new_limit = 5 + new_interval = 300 + + fill_in(limit_label, with: new_limit) + fill_in(interval_label, with: new_interval) + + click_button 'Save changes' + + group.reload + + expect(group.namespace_settings.unique_project_download_limit).to eq new_limit + expect(group.namespace_settings.unique_project_download_limit_interval_in_seconds).to eq new_interval + + expect(page).to have_field(limit_label, with: new_limit) + expect(page).to have_field(interval_label, with: new_interval) + end + + it 'displays validation errors' do + fill_in s_('GroupSettings|Number of projects'), with: -1 + fill_in s_('GroupSettings|Interval (seconds)'), with: -1 + + click_button 'Save changes' + + expect(page).to have_content('Number of projects must be greater than or equal to 0') + expect(page).to have_content('Interval (seconds) must be greater than or equal to 0') + end +end diff --git a/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb b/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb index d28d1f508456ec9cfab946a053ca26649465b72c..1779b7d48aac363464eda5b716cbdc767ac8b0b0 100644 --- a/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb +++ b/ee/spec/lib/ee/sidebars/groups/menus/settings_menu_spec.rb @@ -183,5 +183,22 @@ specify { is_expected.to be_nil } end end + + describe 'Reporting menu' do + let(:item_id) { :reporting } + let(:feature_enabled) { true } + + before do + allow(group).to receive(:unique_project_download_limit_enabled?) { feature_enabled } + end + + it { is_expected.to be_present } + + context 'when feature is not enabled' do + let(:feature_enabled) { false } + + it { is_expected.to be_nil } + end + end end end diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb index 52a0c42ec17e6083b0c4be47783aa02c5728646f..12be8c6c50d41ec03a5553b079db391e0aef72b5 100644 --- a/ee/spec/models/ee/group_spec.rb +++ b/ee/spec/models/ee/group_spec.rb @@ -2542,4 +2542,40 @@ def webhook_headers it { is_expected.to contain_exactly(cluster_agent_for_project, cluster_agent_for_project_in_subgroup) } end + + describe '#unique_project_download_limit_enabled?' do + let_it_be(:group) { create(:group) } + + let(:feature_flag_enabled) { true } + let(:licensed_feature_available) { true } + + before do + stub_feature_flags(limit_unique_project_downloads_per_namespace_user: feature_flag_enabled) + stub_licensed_features(unique_project_download_limit: licensed_feature_available) + end + + subject { group.unique_project_download_limit_enabled? } + + it { is_expected.to eq true } + + context 'when feature flag is disabled' do + let(:feature_flag_enabled) { false } + + it { is_expected.to eq false } + end + + context 'when licensed feature is not available' do + let(:licensed_feature_available) { false } + + it { is_expected.to eq false } + end + + context 'when sub-group' do + let(:subgroup) { create(:group, parent: group) } + + subject { subgroup.unique_project_download_limit_enabled? } + + it { is_expected.to eq false } + end + end end diff --git a/ee/spec/models/namespace_setting_spec.rb b/ee/spec/models/namespace_setting_spec.rb index 0076c55437f8f118f961cd795e6ed618855ee3a4..1d415b8dd0d181c06b45999b564977fa17faae37 100644 --- a/ee/spec/models/namespace_setting_spec.rb +++ b/ee/spec/models/namespace_setting_spec.rb @@ -6,6 +6,25 @@ let(:group) { create(:group) } let(:setting) { group.namespace_settings } + describe 'validations' do + subject(:settings) { group.namespace_settings } + + it { is_expected.to validate_presence_of(:unique_project_download_limit) } + it { is_expected.to validate_presence_of(:unique_project_download_limit_interval_in_seconds) } + it { + is_expected.to validate_numericality_of(:unique_project_download_limit) + .only_integer + .is_greater_than_or_equal_to(0) + .is_less_than_or_equal_to(10_000) + } + it { + is_expected.to validate_numericality_of(:unique_project_download_limit_interval_in_seconds) + .only_integer + .is_greater_than_or_equal_to(0) + .is_less_than_or_equal_to(10.days.to_i) + } + end + describe '#prevent_forking_outside_group?' do context 'with feature available' do before do @@ -220,4 +239,13 @@ end end end + + describe '.allowed_namespace_settings_params' do + it 'includes attributes used for limiting unique project downloads' do + expect(described_class.allowed_namespace_settings_params).to include(*%i[ + unique_project_download_limit + unique_project_download_limit_interval_in_seconds + ]) + end + end end diff --git a/ee/spec/requests/groups/settings/reporting_controller_spec.rb b/ee/spec/requests/groups/settings/reporting_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2c8db608b1ee6d2b6d946a2af4c22d01504d51c8 --- /dev/null +++ b/ee/spec/requests/groups/settings/reporting_controller_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::Settings::ReportingController, type: :request do + let_it_be(:user) { create(:user) } + + let(:group) { create(:group) } + let(:feature_flag_enabled) { true } + let(:licensed_feature_available) { true } + + before do + stub_feature_flags(limit_unique_project_downloads_per_namespace_user: feature_flag_enabled) + stub_licensed_features(unique_project_download_limit: licensed_feature_available) + + sign_in(user) + end + + shared_examples 'renders 404' do + it 'renders 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + end + + shared_examples '404 when feature is unavailable' do + before do + subject + end + + context 'when feature flag is disabled' do + let(:feature_flag_enabled) { false } + + it_behaves_like 'renders 404' + end + + context 'when feature flag is disabled' do + let(:licensed_feature_available) { false } + + it_behaves_like 'renders 404' + end + + context 'when subgroup' do + let(:group) { create(:group, parent: create(:group)) } + + it_behaves_like 'renders 404' + end + end + + describe 'GET /groups/:group_id/-/settings/reporting' do + subject(:request) { get group_settings_reporting_path(group) } + + context 'when user is owner' do + before do + group.add_owner(user) + end + + it 'renders show with 200 status code' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:show) + end + + it_behaves_like '404 when feature is unavailable' + end + + context 'when user is not owner' do + before do + group.add_maintainer(user) + end + + it 'renders a 404' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'PATCH #update' do + let(:params) { { group: { unique_project_download_limit: 10 } } } + + subject(:request) do + patch(group_settings_reporting_path(group), params: params) + end + + context 'when user is not an owner' do + before do + group.add_maintainer(user) + end + + it 'renders a 404' do + request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when user is an owner' do + before do + group.add_owner(user) + end + + it_behaves_like '404 when feature is unavailable' + + it 'redirects back to show page' do + request + + expect(response).to redirect_to(group_settings_reporting_path(group)) + expect(flash[:notice]).to include("Group \"#{group.name}\" was successfully updated.") + end + + context 'update failed' do + let(:params) { { group: { unique_project_download_limit: -1 } } } + + it 're-renders show template' do + request + + expect(response).not_to have_gitlab_http_status(:redirect) + expect(response).to render_template(:show) + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1a5e83516dc2c23a505370450b1eb11c6b749c40..3504f8a06cf337914b43ada88696dd8f6ad2de53 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18014,6 +18014,9 @@ msgstr "" msgid "Group" msgstr "" +msgid "Group \"%{group_name}\" was successfully updated." +msgstr "" + msgid "Group %{group_name} couldn't be exported." msgstr "" @@ -18452,6 +18455,9 @@ msgstr "" msgid "GroupSettings|Auto DevOps pipeline was updated for the group" msgstr "" +msgid "GroupSettings|Automatically ban users who download more than the specified number of projects within the specified interval." +msgstr "" + msgid "GroupSettings|Available only on the top-level group. Applies to all subgroups. Groups already shared with a group outside %{group} are still shared unless removed manually." msgstr "" @@ -18509,9 +18515,15 @@ msgstr "" msgid "GroupSettings|If the parent group's visibility is lower than the group's current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility." msgstr "" +msgid "GroupSettings|Interval (seconds)" +msgstr "" + msgid "GroupSettings|Members cannot invite groups outside of %{group} and its subgroups" msgstr "" +msgid "GroupSettings|Number of projects" +msgstr "" + msgid "GroupSettings|Organizations and contacts can be created and associated with issues." msgstr "" @@ -18530,9 +18542,15 @@ msgstr "" msgid "GroupSettings|Prevent forking setting was not saved" msgstr "" +msgid "GroupSettings|Project download rate limit" +msgstr "" + msgid "GroupSettings|Projects in %{group} cannot be shared with other groups" msgstr "" +msgid "GroupSettings|Reporting" +msgstr "" + msgid "GroupSettings|Select a subgroup to use as the source for custom project templates for this group." msgstr "" @@ -18551,9 +18569,15 @@ msgstr "" msgid "GroupSettings|Set the initial name and protections for the default branch of new repositories created in the group." msgstr "" +msgid "GroupSettings|Set to 0 to disable limiting." +msgstr "" + msgid "GroupSettings|The Auto DevOps pipeline runs if no alternative CI configuration file is found." msgstr "" +msgid "GroupSettings|The maximum number of unique projects a user can download within the specified interval before they're banned. Set to 0 to disable limiting." +msgstr "" + msgid "GroupSettings|The projects in this subgroup can be selected as templates for new projects created in the group. %{link_start}Learn more.%{link_end}" msgstr ""