diff --git a/app/policies/concerns/clusterable_actions.rb b/app/policies/concerns/clusterable_actions.rb new file mode 100644 index 0000000000000000000000000000000000000000..08ddd742ea93f06ec8127e2d65400d11359769c8 --- /dev/null +++ b/app/policies/concerns/clusterable_actions.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module ClusterableActions + private + + # Overridden on EE module + def multiple_clusters_available? + false + end + + def clusterable_has_clusters? + !subject.clusters.empty? + end +end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 6b4e56ef5e412d0faaa6e23ff1514285f471be35..ac98b80dc5c8911545fb8acb1cb5edc12098e48b 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class GroupPolicy < BasePolicy + include ClusterableActions + desc "Group is public" with_options scope: :subject, score: 0 condition(:public_group) { @subject.public? } @@ -27,6 +29,9 @@ class GroupPolicy < BasePolicy GroupProjectsFinder.new(group: @subject, current_user: @user, options: { include_subgroups: true }).execute.any? end + condition(:has_clusters, scope: :subject) { clusterable_has_clusters? } + condition(:can_have_multiple_clusters) { multiple_clusters_available? } + with_options scope: :subject, score: 0 condition(:request_access_enabled) { @subject.request_access_enabled } @@ -44,7 +49,7 @@ class GroupPolicy < BasePolicy enable :read_label end - rule { admin } .enable :read_group + rule { admin }.enable :read_group rule { has_projects }.policy do enable :read_group @@ -66,6 +71,7 @@ class GroupPolicy < BasePolicy enable :admin_pipeline enable :admin_build enable :read_cluster + enable :add_cluster enable :create_cluster enable :update_cluster enable :admin_cluster @@ -105,6 +111,8 @@ class GroupPolicy < BasePolicy rule { owner & (~share_with_group_locked | ~has_parent | ~parent_share_with_group_locked | can_change_parent_share_with_group_lock) }.enable :change_share_with_group_lock + rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster + def access_level return GroupMember::NO_ACCESS if @user.nil? diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 1c08294529949850cfcc0255a486aef1b90e528a..bcbd9676f2eb7c005f74bbdb2e242ae5020c8f35 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -2,6 +2,7 @@ class ProjectPolicy < BasePolicy extend ClassMethods + include ClusterableActions READONLY_FEATURES_WHEN_ARCHIVED = %i[ issue @@ -103,6 +104,9 @@ class ProjectPolicy < BasePolicy @subject.feature_available?(:merge_requests, @user) end + condition(:has_clusters, scope: :subject) { clusterable_has_clusters? } + condition(:can_have_multiple_clusters) { multiple_clusters_available? } + features = %w[ merge_requests issues @@ -257,6 +261,7 @@ class ProjectPolicy < BasePolicy enable :read_pages enable :update_pages enable :read_cluster + enable :add_cluster enable :create_cluster enable :update_cluster enable :admin_cluster @@ -381,6 +386,8 @@ class ProjectPolicy < BasePolicy (can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request) end.enable :read_merge_request_iid + rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster + private def team_member? diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb index 9cc137aa3bdcb5cf6dd6bbaf579f7442134cea61..d94d9118eee7ec9b03f23baafdcc985080bac761 100644 --- a/app/presenters/clusterable_presenter.rb +++ b/app/presenters/clusterable_presenter.rb @@ -12,6 +12,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated .fabricate! end + def can_add_cluster? + can?(current_user, :add_cluster, clusterable) + end + def can_create_cluster? can?(current_user, :create_cluster, clusterable) end diff --git a/app/views/clusters/clusters/_buttons.html.haml b/app/views/clusters/clusters/_buttons.html.haml index 9238903aa10a33423831e72aa47fdb68feaa13bb..c81d1d5b05a987cebfff6f2a2b60dd5340f44645 100644 --- a/app/views/clusters/clusters/_buttons.html.haml +++ b/app/views/clusters/clusters/_buttons.html.haml @@ -1,6 +1,5 @@ --# This partial is overridden in EE .nav-controls - - if clusterable.can_create_cluster? && clusterable.clusters.empty? + - if clusterable.can_add_cluster? = link_to s_('ClusterIntegration|Add Kubernetes cluster'), clusterable.new_path, class: 'btn btn-success js-add-cluster' - else %span.btn.btn-add-cluster.disabled.js-add-cluster diff --git a/app/views/clusters/clusters/_empty_state.html.haml b/app/views/clusters/clusters/_empty_state.html.haml index c926ec258f0f9c97c63aba5dd91e72958d0a910e..cfdbfe2dea144bc8baab852e17711c8c1a5a3f23 100644 --- a/app/views/clusters/clusters/_empty_state.html.haml +++ b/app/views/clusters/clusters/_empty_state.html.haml @@ -9,6 +9,6 @@ = clusterable.empty_state_help_text = clusterable.learn_more_link - - if clusterable.can_create_cluster? + - if clusterable.can_add_cluster? .text-center = link_to s_('ClusterIntegration|Add Kubernetes cluster'), clusterable.new_path, class: 'btn btn-success' diff --git a/changelogs/unreleased/34758-extend-can-create-cluster-logic.yml b/changelogs/unreleased/34758-extend-can-create-cluster-logic.yml new file mode 100644 index 0000000000000000000000000000000000000000..65f5253a27107cc695af242fb74dfbf269d6f26a --- /dev/null +++ b/changelogs/unreleased/34758-extend-can-create-cluster-logic.yml @@ -0,0 +1,5 @@ +--- +title: Allow user to add Kubernetes cluster for clusterable when there are ancestor clusters +merge_request: 23569 +author: +type: other diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 9d0093e81592f1f17f6247abd1dd21ff007331e5..e55401a80cdc65fcc3c3a8e4a2b333bf28234412 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -25,7 +25,8 @@ describe GroupPolicy do :read_cluster, :create_cluster, :update_cluster, - :admin_cluster + :admin_cluster, + :add_cluster ] end @@ -382,4 +383,14 @@ describe GroupPolicy do it { expect_disallowed(:change_share_with_group_lock) } end end + + it_behaves_like 'clusterable policies' do + let(:clusterable) { create(:group) } + let(:cluster) do + create(:cluster, + :provided_by_gcp, + :group, + groups: [clusterable]) + end + end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 69468f9ad8543a782b33583617fd580c58add58f..fa47b95899ad13fca3495837a73ba6f4ca3d2ac1 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -48,7 +48,7 @@ describe ProjectPolicy do update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project admin_commit_status admin_build admin_container_image - admin_pipeline admin_environment admin_deployment + admin_pipeline admin_environment admin_deployment add_cluster ] end @@ -465,4 +465,14 @@ describe ProjectPolicy do expect_disallowed(*maintainer_abilities) end end + + it_behaves_like 'clusterable policies' do + let(:clusterable) { create(:project, :repository) } + let(:cluster) do + create(:cluster, + :provided_by_gcp, + :project, + projects: [clusterable]) + end + end end diff --git a/spec/presenters/clusterable_presenter_spec.rb b/spec/presenters/clusterable_presenter_spec.rb index 4f4ae5e07c5b94fa93460d38a6a2af974d67c862..05afe5347d10e1cd5e2a1ecfed6b093d3bc8fbd1 100644 --- a/spec/presenters/clusterable_presenter_spec.rb +++ b/spec/presenters/clusterable_presenter_spec.rb @@ -14,4 +14,68 @@ describe ClusterablePresenter do expect(subject).to be_kind_of(ProjectClusterablePresenter) end end + + shared_examples 'appropriate member permissions' do + context 'with a developer' do + before do + clusterable.add_developer(user) + end + + it { is_expected.to be_falsy } + end + + context 'with a maintainer' do + before do + clusterable.add_maintainer(user) + end + + it { is_expected.to be_truthy } + end + end + + describe '#can_create_cluster?' do + let(:user) { create(:user) } + + subject { described_class.new(clusterable).can_create_cluster? } + + before do + allow(clusterable).to receive(:current_user).and_return(user) + end + + context 'when clusterable is a group' do + let(:clusterable) { create(:group) } + + it_behaves_like 'appropriate member permissions' + end + + context 'when clusterable is a project' do + let(:clusterable) { create(:project, :repository) } + + it_behaves_like 'appropriate member permissions' + end + end + + describe '#can_add_cluster?' do + let(:user) { create(:user) } + + subject { described_class.new(clusterable).can_add_cluster? } + + before do + clusterable.add_maintainer(user) + + allow(clusterable).to receive(:current_user).and_return(user) + end + + context 'when clusterable is a group' do + let(:clusterable) { create(:group) } + + it_behaves_like 'appropriate member permissions' + end + + context 'when clusterable is a project' do + let(:clusterable) { create(:project, :repository) } + + it_behaves_like 'appropriate member permissions' + end + end end diff --git a/spec/support/shared_examples/policies/clusterable_shared_examples.rb b/spec/support/shared_examples/policies/clusterable_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..d99f94c76c36c6dfb8aa9de2fbe8580d6446534f --- /dev/null +++ b/spec/support/shared_examples/policies/clusterable_shared_examples.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +shared_examples 'clusterable policies' do + describe '#add_cluster?' do + let(:current_user) { create(:user) } + + subject { described_class.new(current_user, clusterable) } + + context 'with a developer' do + before do + clusterable.add_developer(current_user) + end + + it { expect_disallowed(:add_cluster) } + end + + context 'with a maintainer' do + before do + clusterable.add_maintainer(current_user) + end + + context 'with no clusters' do + it { expect_allowed(:add_cluster) } + end + + context 'with an existing cluster' do + before do + cluster + end + + it { expect_disallowed(:add_cluster) } + end + end + end +end