From 4a2df25dfebc602c63baea38af68c3e75e2cb5ec Mon Sep 17 00:00:00 2001
From: Steve Abrams <sabrams@gitlab.com>
Date: Fri, 18 Sep 2020 07:49:58 +0000
Subject: [PATCH] Add form for package limits to admin UI

Form for package max file size plan_limits is
added to the package registry application settings
admin UI page.
---
 .../admin/application_settings_controller.rb  |  1 +
 .../admin/plan_limits_controller.rb           | 39 +++++++++++
 .../_package_registry.html.haml               | 50 ++++++++++++++
 .../240951-package-size-limits-ui.yml         |  5 ++
 config/routes/admin.rb                        |  2 +
 .../settings/continuous_integration.md        | 16 ++++-
 .../_ee_package_registry.haml                 | 14 ++++
 .../_package_registry.haml                    | 23 -------
 locale/gitlab.pot                             | 30 +++++++++
 .../admin/plan_limits_controller_spec.rb      | 45 +++++++++++++
 spec/factories/plan_limits.rb                 |  9 +++
 spec/routing/admin_routing_spec.rb            |  6 ++
 .../_package_registry.html.haml_spec.rb       | 65 +++++++++++++++++++
 13 files changed, 281 insertions(+), 24 deletions(-)
 create mode 100644 app/controllers/admin/plan_limits_controller.rb
 create mode 100644 app/views/admin/application_settings/_package_registry.html.haml
 create mode 100644 changelogs/unreleased/240951-package-size-limits-ui.yml
 create mode 100644 ee/app/views/admin/application_settings/_ee_package_registry.haml
 delete mode 100644 ee/app/views/admin/application_settings/_package_registry.haml
 create mode 100644 spec/controllers/admin/plan_limits_controller_spec.rb
 create mode 100644 spec/views/admin/application_settings/_package_registry.html.haml_spec.rb

diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index fc3d0053859e9..73f71f7ad5592 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -170,6 +170,7 @@ def self_monitoring_data
 
   def set_application_setting
     @application_setting = ApplicationSetting.current_without_cache
+    @plans = Plan.all
   end
 
   def whitelist_query_limiting
diff --git a/app/controllers/admin/plan_limits_controller.rb b/app/controllers/admin/plan_limits_controller.rb
new file mode 100644
index 0000000000000..2620db8aec5ef
--- /dev/null
+++ b/app/controllers/admin/plan_limits_controller.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class Admin::PlanLimitsController < Admin::ApplicationController
+  include InternalRedirect
+
+  before_action :set_plan_limits
+
+  def create
+    redirect_path = referer_path(request) || general_admin_application_settings_path
+
+    respond_to do |format|
+      if @plan_limits.update(plan_limits_params)
+        format.json { head :ok }
+        format.html { redirect_to redirect_path, notice: _('Application limits saved successfully') }
+      else
+        format.json { head :bad_request }
+        format.html { render_update_error }
+      end
+    end
+  end
+
+  private
+
+  def set_plan_limits
+    @plan_limits = Plan.find(plan_limits_params[:plan_id]).actual_limits
+  end
+
+  def plan_limits_params
+    params.require(:plan_limits).permit(%i[
+      plan_id
+      conan_max_file_size
+      maven_max_file_size
+      npm_max_file_size
+      nuget_max_file_size
+      pypi_max_file_size
+      generic_packages_max_file_size
+    ])
+  end
+end
diff --git a/app/views/admin/application_settings/_package_registry.html.haml b/app/views/admin/application_settings/_package_registry.html.haml
new file mode 100644
index 0000000000000..257a90252ccdf
--- /dev/null
+++ b/app/views/admin/application_settings/_package_registry.html.haml
@@ -0,0 +1,50 @@
+- if Gitlab.config.packages.enabled
+  %section.settings.as-package.no-animate#js-package-settings{ class: ('expanded' if expanded_by_default?) }
+    .settings-header
+      %h4
+        = _('Package Registry')
+      %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+        = expanded_by_default? ? _('Collapse') : _('Expand')
+      %p
+        = _("Settings related to the use and experience of using GitLab's Package Registry.")
+
+    = render_if_exists 'admin/application_settings/ee_package_registry'
+
+    .settings-content
+      %h4
+        = _('Package file size limits')
+      %p
+        = _('Set limit to 0 to allow any file size.')
+      .scrolling-tabs-container.inner-page-scroll-tabs
+        - if @plans.size > 1
+          %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs.mb-3
+            - @plans.each_with_index do |plan, index|
+              %li
+                = link_to admin_plan_limits_path(anchor: 'js-package-settings'), data: { target: "div#plan#{index}", action: "plan#{index}", toggle: 'tab'}, class: index == 0 ? 'active': '' do
+                  = plan.name.capitalize
+        .tab-content
+          - @plans.each_with_index do |plan, index|
+            .tab-pane{ :id => "plan#{index}", class: index == 0 ? 'active': '' }
+              = form_for plan.actual_limits, url: admin_plan_limits_path(anchor: 'js-package-settings'), html: { class: 'fieldset-form' }, method: :post do |f|
+                = form_errors(plan)
+                %fieldset
+                  = f.hidden_field(:plan_id, value: plan.id)
+                  .form-group
+                    = f.label :conan_max_file_size, _('Maximum Conan package file size in bytes'), class: 'label-bold'
+                    = f.number_field :conan_max_file_size, class: 'form-control'
+                  .form-group
+                    = f.label :maven_max_file_size, _('Maximum Maven package file size in bytes'), class: 'label-bold'
+                    = f.number_field :maven_max_file_size, class: 'form-control'
+                  .form-group
+                    = f.label :npm_max_file_size, _('Maximum NPM package file size in bytes'), class: 'label-bold'
+                    = f.number_field :npm_max_file_size, class: 'form-control'
+                  .form-group
+                    = f.label :nuget_max_file_size, _('Maximum NuGet package file size in bytes'), class: 'label-bold'
+                    = f.number_field :nuget_max_file_size, class: 'form-control'
+                  .form-group
+                    = f.label :pypi_max_file_size, _('Maximum PyPI package file size in bytes'), class: 'label-bold'
+                    = f.number_field :pypi_max_file_size, class: 'form-control'
+                  .form-group
+                    = f.label :generic_packages_max_file_size, _('Generic package file size in bytes'), class: 'label-bold'
+                    = f.number_field :generic_packages_max_file_size, class: 'form-control'
+                  = f.submit _('Save %{name} size limits').html_safe % { name: plan.name.capitalize }, class: 'btn gl-button btn-success'
diff --git a/changelogs/unreleased/240951-package-size-limits-ui.yml b/changelogs/unreleased/240951-package-size-limits-ui.yml
new file mode 100644
index 0000000000000..5f98146bb889a
--- /dev/null
+++ b/changelogs/unreleased/240951-package-size-limits-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Add admin UI for adjusting package file size limits
+merge_request: 40423
+author:
+type: added
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 1dd1149a9d2d2..bac8247de2e8a 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -141,6 +141,8 @@
     get :status_delete_self_monitoring_project
   end
 
+  resources :plan_limits, only: :create
+
   resources :labels
 
   resources :runners, only: [:index, :show, :update, :destroy] do
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index 607678bfe0eec..b4867d33644cf 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -196,7 +196,9 @@ To set required pipeline configuration:
 
 ![Required pipeline](img/admin_required_pipeline.png)
 
-## Package Registry configuration **(PREMIUM ONLY)**
+## Package Registry configuration
+
+### NPM Forwarding **(PREMIUM ONLY)**
 
 GitLab administrators can disable the forwarding of NPM requests to [npmjs.com](https://www.npmjs.com/).
 
@@ -208,3 +210,15 @@ To disable it:
 1. Click **Save changes**.
 
 ![NPM package requests forwarding](img/admin_package_registry_npm_package_requests_forward.png)
+
+### Package file size limits
+
+GitLab administrators can adjust the maximum allowed file size for each package type.
+
+To set the maximum file size:
+
+1. Go to **Admin Area > Settings > CI/CD**.
+1. Expand the **Package Registry** section.
+1. Find the package type you would like to adjust.
+1. Enter the maximum file size, in bytes.
+1. Click **Save size limits**.
diff --git a/ee/app/views/admin/application_settings/_ee_package_registry.haml b/ee/app/views/admin/application_settings/_ee_package_registry.haml
new file mode 100644
index 0000000000000..ec2c96ef9b769
--- /dev/null
+++ b/ee/app/views/admin/application_settings/_ee_package_registry.haml
@@ -0,0 +1,14 @@
+.settings-content
+  = form_for @application_setting, url: ci_cd_admin_application_settings_path(anchor: 'js-package-settings'), html: { class: 'fieldset-form' } do |f|
+    = form_errors(@application_setting)
+
+    %fieldset
+      .form-group
+        .form-check
+          = f.check_box :npm_package_requests_forwarding, class: 'form-check-input'
+          = f.label :npm_package_requests_forwarding, class: 'form-check-label' do
+            Enable forwarding of NPM package requests to npmjs.org
+            .form-text.text-muted
+              = _("When enabled, if an NPM package isn't found in the GitLab Registry, we will attempt to pull from the global NPM registry.")
+
+    = f.submit _('Save changes'), class: 'btn gl-button btn-success'
diff --git a/ee/app/views/admin/application_settings/_package_registry.haml b/ee/app/views/admin/application_settings/_package_registry.haml
deleted file mode 100644
index 1768a6626b82a..0000000000000
--- a/ee/app/views/admin/application_settings/_package_registry.haml
+++ /dev/null
@@ -1,23 +0,0 @@
-- if Gitlab.config.packages.enabled
-  %section.settings.as-package.no-animate#js-package-settings{ class: ('expanded' if expanded_by_default?) }
-    .settings-header
-      %h4
-        = _('Package Registry')
-      %button.btn.btn-default.js-settings-toggle{ type: 'button' }
-        = expanded_by_default? ? _('Collapse') : _('Expand')
-      %p
-        = _("Settings related to the use and experience of using GitLab's Package Registry.")
-    .settings-content
-      = form_for @application_setting, url: ci_cd_admin_application_settings_path(anchor: 'js-package-settings'), html: { class: 'fieldset-form' } do |f|
-        = form_errors(@application_setting)
-
-        %fieldset
-          .form-group
-            .form-check
-              = f.check_box :npm_package_requests_forwarding, class: 'form-check-input'
-              = f.label :npm_package_requests_forwarding, class: 'form-check-label' do
-                Enable forwarding of NPM package requests to npmjs.org
-                .form-text.text-muted
-                  = _("When enabled, if an NPM package isn't found in the GitLab Registry, we will attempt to pull from the global NPM registry.")
-
-        = f.submit _('Save changes'), class: "btn btn-success"
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3f4b80959970e..249b2a9e67177 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3101,6 +3101,9 @@ msgstr ""
 msgid "Application ID"
 msgstr ""
 
+msgid "Application limits saved successfully"
+msgstr ""
+
 msgid "Application settings saved successfully"
 msgstr ""
 
@@ -11378,6 +11381,9 @@ msgstr ""
 msgid "Generate new token"
 msgstr ""
 
+msgid "Generic package file size in bytes"
+msgstr ""
+
 msgid "Geo"
 msgstr ""
 
@@ -15403,6 +15409,21 @@ msgstr ""
 msgid "Max size 15 MB"
 msgstr ""
 
+msgid "Maximum Conan package file size in bytes"
+msgstr ""
+
+msgid "Maximum Maven package file size in bytes"
+msgstr ""
+
+msgid "Maximum NPM package file size in bytes"
+msgstr ""
+
+msgid "Maximum NuGet package file size in bytes"
+msgstr ""
+
+msgid "Maximum PyPI package file size in bytes"
+msgstr ""
+
 msgid "Maximum Users:"
 msgstr ""
 
@@ -17862,6 +17883,9 @@ msgstr ""
 msgid "Package deleted successfully"
 msgstr ""
 
+msgid "Package file size limits"
+msgstr ""
+
 msgid "Package recipe already exists"
 msgstr ""
 
@@ -21987,6 +22011,9 @@ msgstr ""
 msgid "Save"
 msgstr ""
 
+msgid "Save %{name} size limits"
+msgstr ""
+
 msgid "Save Changes"
 msgstr ""
 
@@ -23048,6 +23075,9 @@ msgstr ""
 msgid "Set iteration"
 msgstr ""
 
+msgid "Set limit to 0 to allow any file size."
+msgstr ""
+
 msgid "Set max session time for web terminal."
 msgstr ""
 
diff --git a/spec/controllers/admin/plan_limits_controller_spec.rb b/spec/controllers/admin/plan_limits_controller_spec.rb
new file mode 100644
index 0000000000000..2666925c2b7d4
--- /dev/null
+++ b/spec/controllers/admin/plan_limits_controller_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::PlanLimitsController do
+  let_it_be(:plan) { create(:plan) }
+  let_it_be(:plan_limits) { create(:plan_limits, plan: plan) }
+
+  describe 'POST create' do
+    let(:params) do
+      {
+        plan_limits: {
+          plan_id: plan.id,
+          conan_max_file_size: file_size, id: plan_limits.id
+        }
+      }
+    end
+
+    context 'with an authenticated admin user' do
+      let(:file_size) { 10.megabytes }
+
+      it 'updates the plan limits', :aggregate_failures do
+        sign_in(create(:admin))
+
+        post :create, params: params
+
+        expect(response).to redirect_to(general_admin_application_settings_path)
+        expect(plan_limits.reload.conan_max_file_size).to eq(file_size)
+      end
+    end
+
+    context 'without admin access' do
+      let(:file_size) { 1.megabytes }
+
+      it 'returns `not_found`' do
+        sign_in(create(:user))
+
+        post :create, params: params
+
+        expect(response).to have_gitlab_http_status(:not_found)
+        expect(plan_limits.conan_max_file_size).not_to eq(file_size)
+      end
+    end
+  end
+end
diff --git a/spec/factories/plan_limits.rb b/spec/factories/plan_limits.rb
index 4aea09618d0c3..ae892307193eb 100644
--- a/spec/factories/plan_limits.rb
+++ b/spec/factories/plan_limits.rb
@@ -7,5 +7,14 @@
     trait :default_plan do
       plan factory: :default_plan
     end
+
+    trait :with_package_file_sizes do
+      conan_max_file_size { 100 }
+      maven_max_file_size { 100 }
+      npm_max_file_size { 100 }
+      nuget_max_file_size { 100 }
+      pypi_max_file_size { 100 }
+      generic_packages_max_file_size { 100 }
+    end
   end
 end
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 396b01edbfa47..fedafff0d1b14 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -178,3 +178,9 @@
     expect(post("/admin/session/destroy")).to route_to('admin/sessions#destroy')
   end
 end
+
+RSpec.describe Admin::PlanLimitsController, "routing" do
+  it "to #create" do
+    expect(post("/admin/plan_limits")).to route_to('admin/plan_limits#create')
+  end
+end
diff --git a/spec/views/admin/application_settings/_package_registry.html.haml_spec.rb b/spec/views/admin/application_settings/_package_registry.html.haml_spec.rb
new file mode 100644
index 0000000000000..ef40829c29b19
--- /dev/null
+++ b/spec/views/admin/application_settings/_package_registry.html.haml_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'admin/application_settings/_package_registry' do
+  let_it_be(:admin) { create(:admin) }
+  let_it_be(:default_plan_limits) { create(:plan_limits, :default_plan, :with_package_file_sizes) }
+  let_it_be(:application_setting) { build(:application_setting) }
+  let(:page) { Capybara::Node::Simple.new(rendered) }
+
+  before do
+    assign(:application_setting, application_setting)
+    allow(view).to receive(:current_user) { admin }
+    allow(view).to receive(:expanded) { true }
+  end
+
+  subject { render partial: 'admin/application_settings/package_registry' }
+
+  context 'package file size limits' do
+    before do
+      assign(:plans, [default_plan_limits.plan])
+    end
+
+    it 'has fields for max package file sizes' do
+      subject
+
+      expect(rendered).to have_field('Maximum Conan package file size in bytes', type: 'number')
+      expect(page.find_field('Maximum Conan package file size in bytes').value).to eq(default_plan_limits.conan_max_file_size.to_s)
+
+      expect(rendered).to have_field('Maximum Maven package file size in bytes', type: 'number')
+      expect(page.find_field('Maximum Maven package file size in bytes').value).to eq(default_plan_limits.maven_max_file_size.to_s)
+
+      expect(rendered).to have_field('Maximum NPM package file size in bytes', type: 'number')
+      expect(page.find_field('Maximum NPM package file size in bytes').value).to eq(default_plan_limits.npm_max_file_size.to_s)
+
+      expect(rendered).to have_field('Maximum NuGet package file size in bytes', type: 'number')
+      expect(page.find_field('Maximum NuGet package file size in bytes').value).to eq(default_plan_limits.nuget_max_file_size.to_s)
+
+      expect(rendered).to have_field('Maximum PyPI package file size in bytes', type: 'number')
+      expect(page.find_field('Maximum PyPI package file size in bytes').value).to eq(default_plan_limits.pypi_max_file_size.to_s)
+    end
+
+    it 'does not display the plan name when there is only one plan' do
+      subject
+
+      expect(page).not_to have_content('Default')
+    end
+  end
+
+  context 'with multiple plans' do
+    let_it_be(:plan) { create(:plan, name: 'Gold') }
+    let_it_be(:gold_plan_limits) { create(:plan_limits, :with_package_file_sizes, plan: plan) }
+
+    before do
+      assign(:plans, [default_plan_limits.plan, gold_plan_limits.plan])
+    end
+
+    it 'displays the plan name when there is more than one plan' do
+      subject
+
+      expect(page).to have_content('Default')
+      expect(page).to have_content('Gold')
+    end
+  end
+end
-- 
GitLab