From 761788697414a4cc82d1583f46e0d9d00bfb8f5c Mon Sep 17 00:00:00 2001 From: nicolasdular <ndular@gitlab.com> Date: Tue, 23 Jun 2020 17:23:59 +0200 Subject: [PATCH] Add Namespace API to set additionally purchased storage This exposes the params `additional_purchased_storage_size` and `additional_purchased_storage_ends_on` in our Namespace API and the possibility to set them for admin users. This API will be used by customer portal when additional storage packages get bought --- app/policies/base_policy.rb | 2 + ee/app/models/ee/namespace.rb | 10 ++++ ee/app/models/namespace_limit.rb | 7 +++ ee/app/policies/ee/group_policy.rb | 2 + ee/app/policies/ee/namespace_policy.rb | 2 + ...asdular-namespace-storage-purchase-api.yml | 5 ++ ee/lib/ee/api/entities/namespace.rb | 7 ++- ee/lib/ee/api/namespaces.rb | 2 + ee/spec/models/ee/namespace_limit_spec.rb | 7 +++ ee/spec/models/ee/namespace_spec.rb | 14 +++++ ee/spec/policies/group_policy_spec.rb | 2 + ee/spec/policies/namespace_policy_spec.rb | 2 + ee/spec/requests/api/namespaces_spec.rb | 54 +++++++++++++++---- .../namespace_policy_shared_examples.rb | 34 ++++++++++++ 14 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 ee/app/models/namespace_limit.rb create mode 100644 ee/changelogs/unreleased/nicolasdular-namespace-storage-purchase-api.yml create mode 100644 ee/spec/models/ee/namespace_limit_spec.rb create mode 100644 spec/support/shared_examples/policies/namespace_policy_shared_examples.rb diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index f41d69c7a41f35..13d732e4eddd5e 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -58,6 +58,8 @@ class BasePolicy < DeclarativePolicy::Base rule { admin }.enable :read_all_resources rule { default }.enable :read_cross_project + + condition(:is_gitlab_com) { ::Gitlab.dev_env_or_com? } end BasePolicy.prepend_if_ee('EE::BasePolicy') diff --git a/ee/app/models/ee/namespace.rb b/ee/app/models/ee/namespace.rb index eb8218fe626b5e..00e07c12f5cebf 100644 --- a/ee/app/models/ee/namespace.rb +++ b/ee/app/models/ee/namespace.rb @@ -26,10 +26,12 @@ module Namespace attr_writer :root_ancestor has_one :namespace_statistics + has_one :namespace_limit, inverse_of: :namespace has_one :gitlab_subscription, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :elasticsearch_indexed_namespace accepts_nested_attributes_for :gitlab_subscription + accepts_nested_attributes_for :namespace_limit scope :include_gitlab_subscription, -> { includes(:gitlab_subscription) } scope :join_gitlab_subscription, -> { joins("LEFT OUTER JOIN gitlab_subscriptions ON gitlab_subscriptions.namespace_id=namespaces.id") } @@ -53,6 +55,10 @@ module Namespace delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset, :extra_shared_runners_minutes, to: :namespace_statistics, allow_nil: true + delegate :additional_purchased_storage_size, :additional_purchased_storage_size=, + :additional_purchased_storage_ends_on, :additional_purchased_storage_ends_on=, + to: :namespace_limit, allow_nil: true + delegate :email, to: :owner, allow_nil: true, prefix: true # Opportunistically clear the +file_template_project_id+ if invalid @@ -72,6 +78,10 @@ module Namespace before_save :clear_feature_available_cache end + def namespace_limit + super.presence || build_namespace_limit + end + class_methods do extend ::Gitlab::Utils::Override diff --git a/ee/app/models/namespace_limit.rb b/ee/app/models/namespace_limit.rb new file mode 100644 index 00000000000000..dfffbf44a1fc6d --- /dev/null +++ b/ee/app/models/namespace_limit.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class NamespaceLimit < ApplicationRecord + self.primary_key = :namespace_id + + belongs_to :namespace, inverse_of: :namespace_limit +end diff --git a/ee/app/policies/ee/group_policy.rb b/ee/app/policies/ee/group_policy.rb index 3c205f0dc1015f..8df2b98142991d 100644 --- a/ee/app/policies/ee/group_policy.rb +++ b/ee/app/policies/ee/group_policy.rb @@ -256,6 +256,8 @@ module GroupPolicy rule { ~reject_unsigned_commits_available }.prevent :change_reject_unsigned_commits rule { can?(:maintainer_access) & push_rules_available }.enable :change_push_rules + + rule { admin & is_gitlab_com }.enable :update_subscription_limit end override :lookup_access_level! diff --git a/ee/app/policies/ee/namespace_policy.rb b/ee/app/policies/ee/namespace_policy.rb index c5dddc0649444f..1743407fef62dc 100644 --- a/ee/app/policies/ee/namespace_policy.rb +++ b/ee/app/policies/ee/namespace_policy.rb @@ -8,6 +8,8 @@ module NamespacePolicy rule { owner | admin }.policy do enable :create_jira_connect_subscription end + + rule { admin & is_gitlab_com }.enable :update_subscription_limit end end end diff --git a/ee/changelogs/unreleased/nicolasdular-namespace-storage-purchase-api.yml b/ee/changelogs/unreleased/nicolasdular-namespace-storage-purchase-api.yml new file mode 100644 index 00000000000000..ef61f9023aac60 --- /dev/null +++ b/ee/changelogs/unreleased/nicolasdular-namespace-storage-purchase-api.yml @@ -0,0 +1,5 @@ +--- +title: Add Namespace API to set additionally purchased storage +merge_request: 35257 +author: +type: added diff --git a/ee/lib/ee/api/entities/namespace.rb b/ee/lib/ee/api/entities/namespace.rb index 86981ec6b97d89..e18237112dd04e 100644 --- a/ee/lib/ee/api/entities/namespace.rb +++ b/ee/lib/ee/api/entities/namespace.rb @@ -7,10 +7,13 @@ module Namespace extend ActiveSupport::Concern prepended do + can_update_limits = ->(namespace, opts) { ::Ability.allowed?(opts[:current_user], :update_subscription_limit, namespace) } can_admin_namespace = ->(namespace, opts) { ::Ability.allowed?(opts[:current_user], :admin_namespace, namespace) } - expose :shared_runners_minutes_limit, if: ->(_, options) { options[:current_user]&.admin? } - expose :extra_shared_runners_minutes_limit, if: ->(_, options) { options[:current_user]&.admin? } + expose :shared_runners_minutes_limit, if: can_update_limits + expose :extra_shared_runners_minutes_limit, if: can_update_limits + expose :additional_purchased_storage_size, if: can_update_limits + expose :additional_purchased_storage_ends_on, if: can_update_limits expose :billable_members_count do |namespace, options| namespace.billable_members_count(options[:requested_hosted_plan]) end diff --git a/ee/lib/ee/api/namespaces.rb b/ee/lib/ee/api/namespaces.rb index 3ed9b6104b7727..11576a60e315d2 100644 --- a/ee/lib/ee/api/namespaces.rb +++ b/ee/lib/ee/api/namespaces.rb @@ -53,6 +53,8 @@ def update_namespace(namespace) params do optional :shared_runners_minutes_limit, type: Integer, desc: "Pipeline minutes quota for this namespace" optional :extra_shared_runners_minutes_limit, type: Integer, desc: "Extra pipeline minutes for this namespace" + optional :additional_purchased_storage_size, type: Integer, desc: "Additional storage size for this namespace" + optional :additional_purchased_storage_ends_on, type: Date, desc: "End of subscription of the additional purchased storage" end put ':id' do authenticated_as_admin! diff --git a/ee/spec/models/ee/namespace_limit_spec.rb b/ee/spec/models/ee/namespace_limit_spec.rb new file mode 100644 index 00000000000000..4e97883f1930b7 --- /dev/null +++ b/ee/spec/models/ee/namespace_limit_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe NamespaceLimit do + it { is_expected.to belong_to(:namespace) } +end diff --git a/ee/spec/models/ee/namespace_spec.rb b/ee/spec/models/ee/namespace_spec.rb index 97e762cc3243fa..6819a6d4632e9b 100644 --- a/ee/spec/models/ee/namespace_spec.rb +++ b/ee/spec/models/ee/namespace_spec.rb @@ -13,6 +13,7 @@ let!(:gold_plan) { create(:gold_plan) } it { is_expected.to have_one(:namespace_statistics) } + it { is_expected.to have_one(:namespace_limit) } it { is_expected.to have_one(:gitlab_subscription).dependent(:destroy) } it { is_expected.to have_one(:elasticsearch_indexed_namespace) } @@ -24,6 +25,10 @@ it { is_expected.to delegate_method(:trial_ends_on).to(:gitlab_subscription) } it { is_expected.to delegate_method(:upgradable?).to(:gitlab_subscription) } it { is_expected.to delegate_method(:email).to(:owner).with_prefix.allow_nil } + it { is_expected.to delegate_method(:additional_purchased_storage_size).to(:namespace_limit) } + it { is_expected.to delegate_method(:additional_purchased_storage_size=).to(:namespace_limit).with_arguments(:args) } + it { is_expected.to delegate_method(:additional_purchased_storage_ends_on).to(:namespace_limit) } + it { is_expected.to delegate_method(:additional_purchased_storage_ends_on=).to(:namespace_limit).with_arguments(:args) } shared_examples 'plan helper' do |namespace_plan| let(:namespace) { create(:namespace_with_plan, plan: "#{plan_name}_plan") } @@ -1462,4 +1467,13 @@ def stub_minutes_used_and_limit(minutes_used, limit) end end end + + describe 'ensure namespace limit' do + it 'has namespace limit upon namespace initialization' do + namespace = build(:namespace) + + expect(namespace.namespace_limit).to be_present + expect(namespace.namespace_limit).not_to be_persisted + end + end end diff --git a/ee/spec/policies/group_policy_spec.rb b/ee/spec/policies/group_policy_spec.rb index 1ea67a4350d579..f2726047304c1f 100644 --- a/ee/spec/policies/group_policy_spec.rb +++ b/ee/spec/policies/group_policy_spec.rb @@ -1086,4 +1086,6 @@ def set_access_level(access_level) end end end + + it_behaves_like 'update namespace limit policy' end diff --git a/ee/spec/policies/namespace_policy_spec.rb b/ee/spec/policies/namespace_policy_spec.rb index 83a943c278ad59..00e77a4f5463e2 100644 --- a/ee/spec/policies/namespace_policy_spec.rb +++ b/ee/spec/policies/namespace_policy_spec.rb @@ -48,4 +48,6 @@ it { is_expected.to be_disallowed(:create_jira_connect_subscription) } end end + + it_behaves_like 'update namespace limit policy' end diff --git a/ee/spec/requests/api/namespaces_spec.rb b/ee/spec/requests/api/namespaces_spec.rb index 4069b653c48a5d..7ecabe83d8e04b 100644 --- a/ee/spec/requests/api/namespaces_spec.rb +++ b/ee/spec/requests/api/namespaces_spec.rb @@ -11,6 +11,10 @@ describe "GET /namespaces" do context "when authenticated as admin" do + before do + allow(Gitlab).to receive(:com?).and_return(true) + end + it "returns correct attributes" do get api("/namespaces", admin) @@ -23,12 +27,14 @@ 'parent_id', 'members_count_with_descendants', 'plan', 'shared_runners_minutes_limit', 'avatar_url', 'web_url', 'trial_ends_on', 'trial', - 'extra_shared_runners_minutes_limit', 'billable_members_count') + 'extra_shared_runners_minutes_limit', 'billable_members_count', + 'additional_purchased_storage_size', 'additional_purchased_storage_ends_on') expect(user_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', 'parent_id', 'plan', 'shared_runners_minutes_limit', 'avatar_url', 'web_url', 'trial_ends_on', 'trial', - 'extra_shared_runners_minutes_limit', 'billable_members_count') + 'extra_shared_runners_minutes_limit', 'billable_members_count', + 'additional_purchased_storage_size', 'additional_purchased_storage_ends_on') end end @@ -111,27 +117,43 @@ end describe 'PUT /namespaces/:id' do + let(:params) do + { + shared_runners_minutes_limit: 9001, + additional_purchased_storage_size: 10_000, + additional_purchased_storage_ends_on: Date.today.to_s + } + end + + before do + allow(Gitlab).to receive(:com?).and_return(true) + end + context 'when authenticated as admin' do it 'updates namespace using full_path when full_path contains dots' do - put api("/namespaces/#{group1.full_path}", admin), params: { shared_runners_minutes_limit: 9001 } + put api("/namespaces/#{group1.full_path}", admin), params: params aggregate_failures do expect(response).to have_gitlab_http_status(:ok) - expect(json_response['shared_runners_minutes_limit']).to eq(9001) + expect(json_response['shared_runners_minutes_limit']).to eq(params[:shared_runners_minutes_limit]) + expect(json_response['additional_purchased_storage_size']).to eq(params[:additional_purchased_storage_size]) + expect(json_response['additional_purchased_storage_ends_on']).to eq(params[:additional_purchased_storage_ends_on]) end end it 'updates namespace using id' do - put api("/namespaces/#{group1.id}", admin), params: { shared_runners_minutes_limit: 9001 } + put api("/namespaces/#{group1.id}", admin), params: params expect(response).to have_gitlab_http_status(:ok) - expect(json_response['shared_runners_minutes_limit']).to eq(9001) + expect(json_response['shared_runners_minutes_limit']).to eq(params[:shared_runners_minutes_limit]) + expect(json_response['additional_purchased_storage_size']).to eq(params[:additional_purchased_storage_size]) + expect(json_response['additional_purchased_storage_ends_on']).to eq(params[:additional_purchased_storage_ends_on]) end end context 'when not authenticated as admin' do it 'retuns 403' do - put api("/namespaces/#{group1.id}", user), params: { shared_runners_minutes_limit: 9001 } + put api("/namespaces/#{group1.id}", user), params: params expect(response).to have_gitlab_http_status(:forbidden) end @@ -139,7 +161,7 @@ context 'when namespace not found' do it 'returns 404' do - put api("/namespaces/#{non_existing_record_id}", admin), params: { shared_runners_minutes_limit: 9001 } + put api("/namespaces/#{non_existing_record_id}", admin), params: params expect(response).to have_gitlab_http_status(:not_found) expect(json_response).to eq('message' => '404 Namespace Not Found') @@ -147,10 +169,20 @@ end context 'when invalid params' do - it 'returns validation error' do - put api("/namespaces/#{group1.id}", admin), params: { shared_runners_minutes_limit: 'unknown' } + where(:attr) do + [ + :shared_runners_minutes_limit, + :additional_purchased_storage_size, + :additional_purchased_storage_ends_on + ] + end - expect(response).to have_gitlab_http_status(:bad_request) + with_them do + it "returns validation error for #{attr}" do + put api("/namespaces/#{group1.id}", admin), params: Hash[attr, 'unknown'] + + expect(response).to have_gitlab_http_status(:bad_request) + end end end diff --git a/spec/support/shared_examples/policies/namespace_policy_shared_examples.rb b/spec/support/shared_examples/policies/namespace_policy_shared_examples.rb new file mode 100644 index 00000000000000..ddec1ba5e3d10f --- /dev/null +++ b/spec/support/shared_examples/policies/namespace_policy_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'update namespace limit policy' do + describe 'update_subscription_limit' do + using RSpec::Parameterized::TableSyntax + + let(:policy) { :update_subscription_limit } + + where(:role, :is_com, :allowed) do + :user | true | false + :owner | true | false + :admin | true | true + :user | false | false + :owner | false | false + :admin | false | false + end + + with_them do + let(:current_user) { build_stubbed(role) } + + before do + allow(Gitlab).to receive(:com?).and_return(is_com) + end + + context 'when admin mode enabled', :enable_admin_mode do + it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } + end + + context 'when admin mode disabled' do + it { is_expected.to be_disallowed(policy) } + end + end + end +end -- GitLab