Commit e8c674b5 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '347471-update-cluster-permissions' into 'master'

Allow developers to read Kubernetes clusters

See merge request gitlab-org/gitlab!77407
parents 248723b0 722985df
<script>
import { GlLink, GlTable, GlIcon, GlSprintf, GlTooltip, GlPopover } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { helpPagePath } from '~/helpers/help_page_helper';
import { AGENT_STATUSES } from '../constants';
import { getAgentConfigPath } from '../clusters_util';
import AgentOptions from './agent_options.vue';
import DeleteAgentButton from './delete_agent_button.vue';
export default {
i18n: {
......@@ -14,7 +14,6 @@ export default {
statusLabel: s__('ClusterAgents|Connection status'),
lastContactLabel: s__('ClusterAgents|Last contact'),
configurationLabel: s__('ClusterAgents|Configuration'),
optionsLabel: __('Options'),
troubleshootingText: s__('ClusterAgents|Learn how to troubleshoot'),
neverConnectedText: s__('ClusterAgents|Never'),
},
......@@ -26,7 +25,7 @@ export default {
GlTooltip,
GlPopover,
TimeAgoTooltip,
AgentOptions,
DeleteAgentButton,
},
mixins: [timeagoMixin],
AGENT_STATUSES,
......@@ -75,7 +74,7 @@ export default {
},
{
key: 'options',
label: this.$options.i18n.optionsLabel,
label: '',
tdClass,
},
];
......@@ -155,7 +154,7 @@ export default {
</template>
<template #cell(options)="{ item }">
<agent-options
<delete-agent-button
:agent="item"
:default-branch-name="defaultBranchName"
:max-agents="maxAgents"
......
<script>
import { GlDropdown, GlDropdownItem, GlModalDirective } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '../constants';
export default {
......@@ -11,8 +11,15 @@ export default {
},
directives: {
GlModalDirective,
GlTooltip: GlTooltipDirective,
},
inject: ['newClusterPath', 'addClusterPath', 'canAddCluster'],
computed: {
tooltip() {
const { connectWithAgent, dropdownDisabledHint } = this.$options.i18n;
return this.canAddCluster ? connectWithAgent : dropdownDisabledHint;
},
},
inject: ['newClusterPath', 'addClusterPath'],
};
</script>
......@@ -20,10 +27,12 @@ export default {
<div class="nav-controls gl-ml-auto">
<gl-dropdown
ref="dropdown"
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
v-gl-modal-directive="canAddCluster && $options.INSTALL_AGENT_MODAL_ID"
v-gl-tooltip="tooltip"
category="primary"
variant="confirm"
:text="$options.i18n.actionsButton"
:disabled="!canAddCluster"
split
right
>
......
......@@ -8,6 +8,7 @@ import {
GlBadge,
GlLoadingIcon,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
import { mapState } from 'vuex';
import {
......@@ -33,6 +34,7 @@ export default {
},
directives: {
GlModalDirective,
GlTooltip: GlTooltipDirective,
},
MAX_CLUSTERS_LIST,
INSTALL_AGENT_MODAL_ID,
......@@ -40,7 +42,7 @@ export default {
agent: AGENT_CARD_INFO,
certificate: CERTIFICATE_BASED_CARD_INFO,
},
inject: ['addClusterPath'],
inject: ['addClusterPath', 'canAddCluster'],
props: {
defaultBranchName: {
default: '.noBranch',
......@@ -91,6 +93,14 @@ export default {
return cardTitle;
},
installAgentTooltip() {
return this.canAddCluster ? '' : this.$options.i18n.agent.installAgentDisabledHint;
},
connectExistingClusterTooltip() {
return this.canAddCluster
? ''
: this.$options.i18n.certificate.connectExistingClusterDisabledHint;
},
},
methods: {
cardFooterNumber(number) {
......@@ -166,13 +176,22 @@ export default {
><gl-sprintf :message="$options.i18n.agent.footerText"
><template #number>{{ cardFooterNumber(totalAgents) }}</template></gl-sprintf
></gl-link
><gl-button
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
class="gl-ml-4"
category="secondary"
variant="confirm"
>{{ $options.i18n.agent.actionText }}</gl-button
>
<div
v-gl-tooltip="installAgentTooltip"
class="gl-display-inline-block"
tabindex="-1"
data-testid="install-agent-button-tooltip"
>
<gl-button
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
class="gl-ml-4"
category="secondary"
variant="confirm"
:disabled="!canAddCluster"
>{{ $options.i18n.agent.actionText }}</gl-button
>
</div>
</template>
</gl-card>
......@@ -206,14 +225,23 @@ export default {
><gl-sprintf :message="$options.i18n.certificate.footerText"
><template #number>{{ cardFooterNumber(totalClusters) }}</template></gl-sprintf
></gl-link
><gl-button
category="secondary"
data-qa-selector="connect_existing_cluster_button"
variant="confirm"
class="gl-ml-4"
:href="addClusterPath"
>{{ $options.i18n.certificate.actionText }}</gl-button
>
<div
v-gl-tooltip="connectExistingClusterTooltip"
class="gl-display-inline-block"
tabindex="-1"
data-testid="connect-existing-cluster-button-tooltip"
>
<gl-button
category="secondary"
data-qa-selector="connect_existing_cluster_button"
variant="confirm"
class="gl-ml-4"
:href="addClusterPath"
:disabled="!canAddCluster"
>{{ $options.i18n.certificate.actionText }}</gl-button
>
</div>
</template>
</gl-card>
</div>
......
<script>
import {
GlDropdown,
GlDropdownItem,
GlButton,
GlModal,
GlModalDirective,
GlSprintf,
GlFormGroup,
GlFormInput,
GlTooltipDirective,
} from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import { DELETE_AGENT_MODAL_ID } from '../constants';
import { sprintf } from '~/locale';
import { DELETE_AGENT_BUTTON, DELETE_AGENT_MODAL_ID } from '../constants';
import deleteAgent from '../graphql/mutations/delete_agent.mutation.graphql';
import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import { removeAgentFromStore } from '../graphql/cache_update';
export default {
i18n: {
dropdownText: __('More options'),
deleteButton: s__('ClusterAgents|Delete agent'),
modalTitle: __('Are you sure?'),
modalBody: s__(
'ClusterAgents|Are you sure you want to delete this agent? You cannot undo this.',
),
modalInputLabel: s__('ClusterAgents|To delete the agent, type %{name} to confirm:'),
modalAction: s__('ClusterAgents|Delete'),
modalCancel: __('Cancel'),
successMessage: s__('ClusterAgents|%{name} successfully deleted'),
defaultError: __('An error occurred. Please try again.'),
},
i18n: DELETE_AGENT_BUTTON,
components: {
GlDropdown,
GlDropdownItem,
GlButton,
GlModal,
GlSprintf,
GlFormGroup,
......@@ -38,8 +25,9 @@ export default {
},
directives: {
GlModalDirective,
GlTooltip: GlTooltipDirective,
},
inject: ['projectPath'],
inject: ['projectPath', 'canAdminCluster'],
props: {
agent: {
required: true,
......@@ -66,6 +54,13 @@ export default {
};
},
computed: {
deleteButtonDisabled() {
return this.loading || !this.canAdminCluster;
},
deleteButtonTooltip() {
const { deleteButton, disabledHint } = this.$options.i18n;
return this.deleteButtonDisabled ? disabledHint : deleteButton;
},
getAgentsQueryVariables() {
return {
defaultBranchName: this.defaultBranchName,
......@@ -159,19 +154,22 @@ export default {
<template>
<div>
<gl-dropdown
icon="ellipsis_v"
right
:disabled="loading"
:text="$options.i18n.dropdownText"
text-sr-only
category="tertiary"
no-caret
<div
v-gl-tooltip="deleteButtonTooltip"
class="gl-display-inline-block"
tabindex="-1"
data-testid="delete-agent-button-tooltip"
>
<gl-dropdown-item v-gl-modal-directive="modalId">
{{ $options.i18n.deleteButton }}
</gl-dropdown-item>
</gl-dropdown>
<gl-button
ref="deleteAgentButton"
v-gl-modal-directive="modalId"
icon="remove"
category="secondary"
variant="danger"
:disabled="deleteButtonDisabled"
:aria-label="$options.i18n.deleteButton"
/>
</div>
<gl-modal
ref="modal"
......
......@@ -190,6 +190,9 @@ export const AGENT_CARD_INFO = {
},
actionText: s__('ClusterAgents|Install new Agent'),
footerText: sprintf(s__('ClusterAgents|View all %{number} agents')),
installAgentDisabledHint: s__(
'ClusterAgents|Requires a Maintainer or greater role to install new agents',
),
};
export const CERTIFICATE_BASED_CARD_INFO = {
......@@ -201,6 +204,9 @@ export const CERTIFICATE_BASED_CARD_INFO = {
actionText: s__('ClusterAgents|Connect existing cluster'),
footerText: sprintf(s__('ClusterAgents|View all %{number} clusters')),
badgeText: s__('ClusterAgents|Deprecated'),
connectExistingClusterDisabledHint: s__(
'ClusterAgents|Requires a maintainer or greater role to connect existing clusters',
),
};
export const MAX_CLUSTERS_LIST = 6;
......@@ -226,8 +232,23 @@ export const CLUSTERS_TABS = [
export const CLUSTERS_ACTIONS = {
actionsButton: s__('ClusterAgents|Actions'),
createNewCluster: s__('ClusterAgents|Create a new cluster'),
connectWithAgent: s__('ClusterAgents|Connect with Agent'),
connectWithAgent: s__('ClusterAgents|Connect with agent'),
connectExistingCluster: s__('ClusterAgents|Connect with a certificate'),
dropdownDisabledHint: s__(
'ClusterAgents|Requires a Maintainer or greater role to perform these actions',
),
};
export const DELETE_AGENT_BUTTON = {
deleteButton: s__('ClusterAgents|Delete agent'),
disabledHint: s__('ClusterAgents|Requires a Maintainer or greater role to delete agents'),
modalTitle: __('Are you sure?'),
modalBody: s__('ClusterAgents|Are you sure you want to delete this agent? You cannot undo this.'),
modalInputLabel: s__('ClusterAgents|To delete the agent, type %{name} to confirm:'),
modalAction: s__('ClusterAgents|Delete'),
modalCancel: __('Cancel'),
successMessage: s__('ClusterAgents|%{name} successfully deleted'),
defaultError: __('An error occurred. Please try again.'),
};
export const AGENT = 'agent';
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { parseBoolean } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import ClustersMainView from './components/clusters_main_view.vue';
import { createStore } from './store';
......@@ -24,6 +25,8 @@ export default () => {
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster,
canAdminCluster,
} = el.dataset;
return new Vue({
......@@ -37,6 +40,8 @@ export default () => {
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster: parseBoolean(canAddCluster),
canAdminCluster: parseBoolean(canAdminCluster),
},
store: createStore(el.dataset),
render(createElement) {
......
......@@ -4,7 +4,7 @@ class Clusters::BaseController < ApplicationController
include RoutableActions
skip_before_action :authenticate_user!
before_action :authorize_read_cluster!
before_action :authorize_admin_cluster!, except: [:show, :index, :new, :authorize_aws_role, :update]
helper_method :clusterable
......@@ -18,11 +18,11 @@ class Clusters::BaseController < ApplicationController
end
def authorize_update_cluster!
access_denied! unless can?(current_user, :update_cluster, cluster)
access_denied! unless can?(current_user, :update_cluster, clusterable)
end
def authorize_admin_cluster!
access_denied! unless can?(current_user, :admin_cluster, cluster)
access_denied! unless can?(current_user, :admin_cluster, clusterable)
end
def authorize_read_cluster!
......
......@@ -10,9 +10,9 @@ class Clusters::ClustersController < Clusters::BaseController
before_action :validate_gcp_token, only: [:new]
before_action :gcp_cluster, only: [:new]
before_action :user_cluster, only: [:new]
before_action :authorize_read_cluster!, only: [:show, :index]
before_action :authorize_create_cluster!, only: [:new, :authorize_aws_role]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy, :clear_cache]
before_action :update_applications_status, only: [:cluster_status]
helper_method :token_in_session
......
......@@ -16,7 +16,7 @@ class Projects::ClusterAgentsController < Projects::ApplicationController
private
def authorize_can_read_cluster_agent!
return if can?(current_user, :admin_cluster, project)
return if can?(current_user, :read_cluster, project)
access_denied!
end
......
......@@ -25,7 +25,7 @@ module Resolvers
private
def can_read_agent_tokens?
current_user.can?(:admin_cluster, project)
current_user.can?(:read_cluster, project)
end
end
end
......
......@@ -21,7 +21,7 @@ module Resolvers
private
def can_read_agent_configuration?
current_user.can?(:admin_cluster, project)
current_user.can?(:read_cluster, project)
end
def kas_client
......
......@@ -5,7 +5,7 @@ module Types
class AgentActivityEventType < BaseObject
graphql_name 'ClusterAgentActivityEvent'
authorize :admin_cluster
authorize :read_cluster
connection_type_class(Types::CountableConnectionType)
......
......@@ -5,7 +5,7 @@ module Types
class AgentTokenType < BaseObject
graphql_name 'ClusterAgentToken'
authorize :admin_cluster
authorize :read_cluster
connection_type_class(Types::CountableConnectionType)
......
......@@ -5,7 +5,7 @@ module Types
class AgentType < BaseObject
graphql_name 'ClusterAgent'
authorize :admin_cluster
authorize :read_cluster
connection_type_class(Types::CountableConnectionType)
......
......@@ -28,7 +28,8 @@ module ClustersHelper
clusters_empty_state_image: image_path('illustrations/empty-state/empty-state-clusters.svg'),
empty_state_help_text: clusterable.empty_state_help_text,
new_cluster_path: clusterable.new_path(tab: 'create'),
can_add_cluster: clusterable.can_add_cluster?.to_s
can_add_cluster: clusterable.can_add_cluster?.to_s,
can_admin_cluster: clusterable.can_admin_cluster?.to_s
}
end
......
......@@ -144,6 +144,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :developer_access
enable :admin_crm_organization
enable :admin_crm_contact
enable :read_cluster
end
rule { reporter }.policy do
......@@ -166,7 +167,6 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :create_projects
enable :admin_pipeline
enable :admin_build
enable :read_cluster
enable :add_cluster
enable :create_cluster
enable :update_cluster
......
......@@ -385,6 +385,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_environment
enable :create_deployment
enable :update_deployment
enable :read_cluster
enable :create_release
enable :update_release
enable :destroy_release
......@@ -433,7 +434,6 @@ class ProjectPolicy < BasePolicy
enable :read_pages
enable :update_pages
enable :remove_pages
enable :read_cluster
enable :add_cluster
enable :create_cluster
enable :update_cluster
......
......@@ -16,6 +16,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
can?(current_user, :add_cluster, clusterable)
end
def can_admin_cluster?
can?(current_user, :admin_cluster, clusterable)
end
def can_create_cluster?
can?(current_user, :create_cluster, clusterable)
end
......
......@@ -78,6 +78,7 @@ The following table lists project permissions available for each role:
| [CI/CD](../ci/index.md):<br>Use [environment terminals](../ci/environments/index.md#web-terminals-deprecated) | | | | ✓ | ✓ |
| [CI/CD](../ci/index.md):<br>Delete pipelines | | | | | ✓ |
| [Clusters](infrastructure/clusters/index.md):<br>View [pod logs](project/clusters/kubernetes_pod_logs.md) | | | ✓ | ✓ | ✓ |
| [Clusters](infrastructure/clusters/index.md):<br>View clusters | | | ✓ | ✓ | ✓ |
| [Clusters](infrastructure/clusters/index.md):<br>Manage clusters | | | | ✓ | ✓ |
| [Container Registry](packages/container_registry/index.md):<br>Create, edit, delete cleanup policies | | | ✓ | ✓ | ✓ |
| [Container Registry](packages/container_registry/index.md):<br>Remove a container registry image | | | ✓ | ✓ | ✓ |
......
......@@ -210,33 +210,8 @@ RSpec.describe 'Query.vulnerabilities.location' do
expect(location['kubernetesResource']['name']).to eq('nginx-deployment')
expect(location['kubernetesResource']['containerName']).to eq('nginx')
expect(location['kubernetesResource']['clusterId']).to eq('gid://gitlab/Clusters::Cluster/1')
end
context 'when user is not authorized to administrate clusters' do
before do
project.add_developer(user)
post_graphql(query, current_user: user)
end
it 'does not return agent data' do
location = subject.first['location']
expect(location['kubernetesResource']['agent']).to be_nil
end
end
context 'when user is authorized to administrate clusters' do
before do
project.add_maintainer(user)
post_graphql(query, current_user: user)
end
it 'returns agent data' do
location = subject.first['location']
expect(location['kubernetesResource']['agent']['id']).to eq("gid://gitlab/Clusters::Agent/#{agent.id}")
expect(location['kubernetesResource']['agent']['name']).to eq(agent.name)
end
expect(location['kubernetesResource']['agent']['id']).to eq("gid://gitlab/Clusters::Agent/#{agent.id}")
expect(location['kubernetesResource']['agent']['name']).to eq(agent.name)
end
end
......
......@@ -7605,10 +7605,10 @@ msgstr ""
msgid "ClusterAgents|Connect existing cluster"
msgstr ""
msgid "ClusterAgents|Connect with Agent"
msgid "ClusterAgents|Connect with a certificate"
msgstr ""
msgid "ClusterAgents|Connect with a certificate"
msgid "ClusterAgents|Connect with agent"
msgstr ""
msgid "ClusterAgents|Connect with the GitLab Agent"
......@@ -7725,6 +7725,18 @@ msgstr ""
msgid "ClusterAgents|Registration token"
msgstr ""
msgid "ClusterAgents|Requires a Maintainer or greater role to delete agents"
msgstr ""
msgid "ClusterAgents|Requires a Maintainer or greater role to install new agents"
msgstr ""
msgid "ClusterAgents|Requires a Maintainer or greater role to perform these actions"
msgstr ""
msgid "ClusterAgents|Requires a maintainer or greater role to connect existing clusters"
msgstr ""
msgid "ClusterAgents|Security"
msgstr ""
......@@ -23243,9 +23255,6 @@ msgstr ""
msgid "More information."
msgstr ""
msgid "More options"
msgstr ""
msgid "More than %{number_commits_distance} commits different with %{default_branch}"
msgstr ""
......
......@@ -103,7 +103,7 @@ RSpec.describe Groups::ClustersController do
it('is denied for admin when admin mode is disabled') { expect { go }.to be_denied_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
it { expect { go }.to be_denied_for(:developer).of(group) }
it { expect { go }.to be_allowed_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
......@@ -673,7 +673,7 @@ RSpec.describe Groups::ClustersController do
it('is denied for admin when admin mode is disabled') { expect { go }.to be_denied_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(group) }
it { expect { go }.to be_allowed_for(:maintainer).of(group) }
it { expect { go }.to be_denied_for(:developer).of(group) }
it { expect { go }.to be_allowed_for(:developer).of(group) }
it { expect { go }.to be_denied_for(:reporter).of(group) }
it { expect { go }.to be_denied_for(:guest).of(group) }
it { expect { go }.to be_denied_for(:user) }
......
......@@ -101,7 +101,7 @@ RSpec.describe Projects::ClustersController do
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:maintainer).of(project) }
it { expect { go }.to be_denied_for(:developer).of(project) }
it { expect { go }.to be_allowed_for(:developer).of(project) }
it { expect { go }.to be_denied_for(:reporter).of(project) }
it { expect { go }.to be_denied_for(:guest).of(project) }
it { expect { go }.to be_denied_for(:user) }
......@@ -711,7 +711,7 @@ RSpec.describe Projects::ClustersController do
end
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:maintainer).of(project) }
it { expect { go }.to be_denied_for(:developer).of(project) }
it { expect { go }.to be_allowed_for(:developer).of(project) }
it { expect { go }.to be_denied_for(:reporter).of(project) }
it { expect { go }.to be_denied_for(:guest).of(project) }
it { expect { go }.to be_denied_for(:user) }
......
......@@ -117,9 +117,8 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
expect(page).to have_link('Error Tracking', href: project_error_tracking_index_path(project))
expect(page).to have_link('Product Analytics', href: project_product_analytics_path(project))
expect(page).to have_link('Logs', href: project_logs_path(project))
expect(page).not_to have_link('Serverless', href: project_serverless_functions_path(project))
expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project))
expect(page).to have_link('Serverless', href: project_serverless_functions_path(project))
expect(page).to have_link('Kubernetes', href: project_clusters_path(project))
end
it_behaves_like 'shows Monitor menu based on the access level'
......
......@@ -615,7 +615,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
end
context 'when the user is not able to view the cluster' do
let(:user_access_level) { :developer }
let(:user_access_level) { :reporter }
it 'includes only the name of the cluster without a link' do
expect(page).to have_content 'using cluster the-cluster'
......
......@@ -15,7 +15,11 @@ RSpec.describe Clusters::AgentsFinder do
it { is_expected.to contain_exactly(matching_agent) }
context 'user does not have permission' do
let(:user) { create(:user, developer_projects: [project]) }
let(:user) { create(:user) }
before do
project.add_reporter(user)
end
it { is_expected.to be_empty }
end
......
import { GlLink, GlIcon } from '@gitlab/ui';
import AgentTable from '~/clusters_list/components/agent_table.vue';
import AgentOptions from '~/clusters_list/components/agent_options.vue';
import DeleteAgentButton from '~/clusters_list/components/delete_agent_button.vue';
import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
......@@ -56,7 +56,7 @@ const propsData = {
],
};
const AgentOptionsStub = stubComponent(AgentOptions, {
const DeleteAgentButtonStub = stubComponent(DeleteAgentButton, {
template: `<div></div>`,
});
......@@ -69,14 +69,14 @@ describe('AgentTable', () => {
const findLastContactText = (at) => wrapper.findAllByTestId('cluster-agent-last-contact').at(at);
const findConfiguration = (at) =>
wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
const findAgentOptions = () => wrapper.findAllComponents(AgentOptions);
const findDeleteAgentButton = () => wrapper.findAllComponents(DeleteAgentButton);
beforeEach(() => {
wrapper = mountExtended(AgentTable, {
propsData,
provide: provideData,
stubs: {
AgentOptions: AgentOptionsStub,
DeleteAgentButton: DeleteAgentButtonStub,
},
});
});
......@@ -128,7 +128,7 @@ describe('AgentTable', () => {
});
it('displays actions menu for each agent', () => {
expect(findAgentOptions()).toHaveLength(3);
expect(findDeleteAgentButton()).toHaveLength(3);
});
});
});
......@@ -10,9 +10,10 @@ describe('ClustersActionsComponent', () => {
const newClusterPath = 'path/to/create/cluster';
const addClusterPath = 'path/to/connect/existing/cluster';
const provideData = {
const defaultProvide = {
newClusterPath,
addClusterPath,
canAddCluster: true,
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
......@@ -21,13 +22,21 @@ describe('ClustersActionsComponent', () => {
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
beforeEach(() => {
const createWrapper = (provideData = {}) => {
wrapper = shallowMountExtended(ClustersActions, {
provide: provideData,
provide: {
...defaultProvide,
...provideData,
},
directives: {
GlModalDirective: createMockDirective(),
GlTooltip: createMockDirective(),
},
});
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
......@@ -52,4 +61,24 @@ describe('ClustersActionsComponent', () => {
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
describe('when user cannot add clusters', () => {
beforeEach(() => {
createWrapper({ canAddCluster: false });
});
it('disables dropdown', () => {
expect(findDropdown().props('disabled')).toBe(true);
});
it('shows tooltip explaining why dropdown is disabled', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint);
});
});
});
......@@ -32,8 +32,9 @@ describe('ClustersViewAllComponent', () => {
defaultBranchName,
};
const provideData = {
const defaultProvide = {
addClusterPath,
canAddCluster: true,
};
const entryData = {
......@@ -45,31 +46,43 @@ describe('ClustersViewAllComponent', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAgentsComponent = () => wrapper.findComponent(Agents);
const findClustersComponent = () => wrapper.findComponent(Clusters);
const findInstallAgentButtonTooltip = () => wrapper.findByTestId('install-agent-button-tooltip');
const findConnectExistingClusterButtonTooltip = () =>
wrapper.findByTestId('connect-existing-cluster-button-tooltip');
const findCardsContainer = () => wrapper.findByTestId('clusters-cards-container');
const findAgentCardTitle = () => wrapper.findByTestId('agent-card-title');
const findRecommendedBadge = () => wrapper.findComponent(GlBadge);
const findClustersCardTitle = () => wrapper.findByTestId('clusters-card-title');
const findFooterButton = (line) => findCards().at(line).findComponent(GlButton);
const getTooltipText = (el) => {
const binding = getBinding(el, 'gl-tooltip');
return binding.value;
};
const createStore = (initialState) =>
new Vuex.Store({
state: initialState,
});
const createWrapper = ({ initialState }) => {
const createWrapper = ({ initialState = entryData, provideData } = {}) => {
wrapper = shallowMountExtended(ClustersViewAll, {
store: createStore(initialState),
propsData,
provide: provideData,
provide: {
...defaultProvide,
...provideData,
},
directives: {
GlModalDirective: createMockDirective(),
GlTooltip: createMockDirective(),
},
stubs: { GlCard, GlSprintf },
});
};
beforeEach(() => {
createWrapper({ initialState: entryData });
createWrapper();
});
afterEach(() => {
......@@ -125,15 +138,20 @@ describe('ClustersViewAllComponent', () => {
expect(findAgentsComponent().props('defaultBranchName')).toBe(defaultBranchName);
});
it('should show install new Agent button in the footer', () => {
expect(findFooterButton(0).exists()).toBe(true);
expect(findFooterButton(0).props('disabled')).toBe(false);
});
it('does not show tooltip for install new Agent button', () => {
expect(getTooltipText(findInstallAgentButtonTooltip().element)).toBe('');
});
describe('when there are no agents', () => {
it('should show the empty title', () => {
expect(findAgentCardTitle().text()).toBe(AGENT_CARD_INFO.emptyTitle);
});
it('should show install new Agent button in the footer', () => {
expect(findFooterButton(0).exists()).toBe(true);
});
it('should render correct modal id for the agent link', () => {
const binding = getBinding(findFooterButton(0).element, 'gl-modal-directive');
......@@ -173,6 +191,22 @@ describe('ClustersViewAllComponent', () => {
});
});
});
describe('when the user cannot add clusters', () => {
beforeEach(() => {
createWrapper({ provideData: { canAddCluster: false } });
});
it('should disable the button', () => {
expect(findFooterButton(0).props('disabled')).toBe(true);
});
it('should show a tooltip explaining why the button is disabled', () => {
expect(getTooltipText(findInstallAgentButtonTooltip().element)).toBe(
AGENT_CARD_INFO.installAgentDisabledHint,
);
});
});
});
describe('clusters tab', () => {
......@@ -189,13 +223,34 @@ describe('ClustersViewAllComponent', () => {
expect(findClustersCardTitle().text()).toBe(CERTIFICATE_BASED_CARD_INFO.emptyTitle);
});
it('should show install new Agent button in the footer', () => {
it('should show install new cluster button in the footer', () => {
expect(findFooterButton(1).exists()).toBe(true);
expect(findFooterButton(1).props('disabled')).toBe(false);
});
it('should render correct href for the button in the footer', () => {
expect(findFooterButton(1).attributes('href')).toBe(addClusterPath);
});
it('does not show tooltip for install new cluster button', () => {
expect(getTooltipText(findConnectExistingClusterButtonTooltip().element)).toBe('');
});
});
describe('when the user cannot add clusters', () => {
beforeEach(() => {
createWrapper({ provideData: { canAddCluster: false } });
});
it('should disable the button', () => {
expect(findFooterButton(1).props('disabled')).toBe(true);
});
it('should show a tooltip explaining why the button is disabled', () => {
expect(getTooltipText(findConnectExistingClusterButtonTooltip().element)).toBe(
CERTIFICATE_BASED_CARD_INFO.connectExistingClusterDisabledHint,
);
});
});
describe('when the clusters are present', () => {
......
import { GlDropdown, GlDropdownItem, GlModal, GlFormInput } from '@gitlab/ui';
import { GlButton, GlModal, GlFormInput } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
......@@ -7,8 +7,9 @@ import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.gra
import deleteAgentMutation from '~/clusters_list/graphql/mutations/delete_agent.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import AgentOptions from '~/clusters_list/components/agent_options.vue';
import { MAX_LIST_COUNT } from '~/clusters_list/constants';
import DeleteAgentButton from '~/clusters_list/components/delete_agent_button.vue';
import { MAX_LIST_COUNT, DELETE_AGENT_BUTTON } from '~/clusters_list/constants';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getAgentResponse, mockDeleteResponse, mockErrorDeleteResponse } from '../mocks/apollo';
Vue.use(VueApollo);
......@@ -22,18 +23,23 @@ const agent = {
webPath: 'agent-webPath',
};
describe('AgentOptions', () => {
describe('DeleteAgentButton', () => {
let wrapper;
let toast;
let apolloProvider;
let deleteResponse;
const findModal = () => wrapper.findComponent(GlModal);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDeleteBtn = () => wrapper.findComponent(GlDropdownItem);
const findDeleteBtn = () => wrapper.findComponent(GlButton);
const findInput = () => wrapper.findComponent(GlFormInput);
const findPrimaryAction = () => findModal().props('actionPrimary');
const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
const findDeleteAgentButtonTooltip = () => wrapper.findByTestId('delete-agent-button-tooltip');
const getTooltipText = (el) => {
const binding = getBinding(el, 'gl-tooltip');
return binding.value;
};
const createMockApolloProvider = ({ mutationResponse }) => {
deleteResponse = jest.fn().mockResolvedValue(mutationResponse);
......@@ -54,10 +60,14 @@ describe('AgentOptions', () => {
});
};
const createWrapper = async ({ mutationResponse = mockDeleteResponse } = {}) => {
const createWrapper = async ({
mutationResponse = mockDeleteResponse,
provideData = {},
} = {}) => {
apolloProvider = createMockApolloProvider({ mutationResponse });
const provide = {
const defaultProvide = {
projectPath,
canAdminCluster: true,
};
const propsData = {
defaultBranchName,
......@@ -67,9 +77,15 @@ describe('AgentOptions', () => {
toast = jest.fn();
wrapper = shallowMountExtended(AgentOptions, {
wrapper = shallowMountExtended(DeleteAgentButton, {
apolloProvider,
provide,
provide: {
...defaultProvide,
...provideData,
},
directives: {
GlTooltip: createMockDirective(),
},
propsData,
mocks: { $toast: { show: toast } },
stubs: { GlModal },
......@@ -100,7 +116,13 @@ describe('AgentOptions', () => {
describe('delete agent action', () => {
it('displays a delete button', () => {
expect(findDeleteBtn().text()).toBe('Delete agent');
expect(findDeleteBtn().attributes('aria-label')).toBe(DELETE_AGENT_BUTTON.deleteButton);
});
it('shows a tooltip for the button', () => {
expect(getTooltipText(findDeleteAgentButtonTooltip().element)).toBe(
DELETE_AGENT_BUTTON.deleteButton,
);
});
describe('when clicking the delete button', () => {
......@@ -113,6 +135,22 @@ describe('AgentOptions', () => {
});
});
describe('when user cannot delete clusters', () => {
beforeEach(() => {
createWrapper({ provideData: { canAdminCluster: false } });
});
it('disables the button', () => {
expect(findDeleteBtn().attributes('disabled')).toBe('true');
});
it('shows a disabled tooltip', () => {
expect(getTooltipText(findDeleteAgentButtonTooltip().element)).toBe(
DELETE_AGENT_BUTTON.disabledHint,
);
});
});
describe.each`
condition | agentName | isDisabled | mutationCalled
${'the input with agent name is missing'} | ${''} | ${true} | ${false}
......@@ -191,14 +229,14 @@ describe('AgentOptions', () => {
await submitAgentToDelete();
});
it('reenables the options dropdown', async () => {
it('reenables the button', async () => {
expect(findPrimaryActionAttributes('loading')).toBe(true);
expect(findDropdown().attributes('disabled')).toBe('true');
expect(findDeleteBtn().attributes('disabled')).toBe('true');
await findModal().vm.$emit('hide');
expect(findPrimaryActionAttributes('loading')).toBe(false);
expect(findDropdown().attributes('disabled')).toBeUndefined();
expect(findDeleteBtn().attributes('disabled')).toBeUndefined();
});
it('clears the agent name input', async () => {
......
......@@ -11,7 +11,7 @@ RSpec.describe Resolvers::Clusters::AgentTokensResolver do
describe '#resolve' do
let(:agent) { create(:cluster_agent) }
let(:user) { create(:user, maintainer_projects: [agent.project]) }
let(:user) { create(:user, developer_projects: [agent.project]) }
let(:ctx) { Hash(current_user: user) }
let!(:matching_token1) { create(:cluster_agent_token, agent: agent, last_used_at: 5.days.ago) }
......@@ -33,7 +33,11 @@ RSpec.describe Resolvers::Clusters::AgentTokensResolver do
end
context 'user does not have permission' do
let(:user) { create(:user, developer_projects: [agent.project]) }
let(:user) { create(:user) }
before do
agent.project.add_reporter(user)
end
it { is_expected.to be_empty }
end
......
......@@ -15,10 +15,14 @@ RSpec.describe Resolvers::Clusters::AgentsResolver do
describe '#resolve' do
let_it_be(:project) { create(:project) }
let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) }
let_it_be(:developer) { create(:user, developer_projects: [project]) }
let_it_be(:maintainer) { create(:user, developer_projects: [project]) }
let_it_be(:reporter) { create(:user) }
let_it_be(:agents) { create_list(:cluster_agent, 2, project: project) }
before do
project.add_reporter(reporter)
end
let(:ctx) { { current_user: current_user } }
subject { resolve_agents }
......@@ -32,7 +36,7 @@ RSpec.describe Resolvers::Clusters::AgentsResolver do
end
context 'the current user does not have access to clusters' do
let(:current_user) { developer }
let(:current_user) { reporter }
it 'returns an empty result' do
expect(subject).to be_empty
......
......@@ -6,6 +6,6 @@ RSpec.describe GitlabSchema.types['ClusterAgentActivityEvent'] do
let(:fields) { %i[recorded_at kind level user agent_token] }
it { expect(described_class.graphql_name).to eq('ClusterAgentActivityEvent') }
it { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
it { expect(described_class).to require_graphql_authorizations(:read_cluster) }
it { expect(described_class).to have_graphql_fields(fields) }
end
......@@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['ClusterAgentToken'] do
it { expect(described_class.graphql_name).to eq('ClusterAgentToken') }
it { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
it { expect(described_class).to require_graphql_authorizations(:read_cluster) }
it { expect(described_class).to have_graphql_fields(fields) }
end
......@@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['ClusterAgent'] do
it { expect(described_class.graphql_name).to eq('ClusterAgent') }
it { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
it { expect(described_class).to require_graphql_authorizations(:read_cluster) }
it { expect(described_class).to have_graphql_fields(fields) }
end
......@@ -93,8 +93,9 @@ RSpec.describe ClustersHelper do
end
context 'user has no permissions to create a cluster' do
it 'displays that user can\t add cluster' do
it 'displays that user can\'t add cluster' do
expect(subject[:can_add_cluster]).to eq("false")
expect(subject[:can_admin_cluster]).to eq("false")
end
end
......@@ -105,6 +106,7 @@ RSpec.describe ClustersHelper do
it 'displays that the user can add cluster' do
expect(subject[:can_add_cluster]).to eq("true")
expect(subject[:can_admin_cluster]).to eq("true")
end
end
......
......@@ -10,13 +10,22 @@ RSpec.describe Clusters::AgentTokenPolicy do
let(:project) { token.agent.project }
describe 'rules' do
context 'when reporter' do
before do
project.add_reporter(user)
end
it { expect(policy).to be_disallowed :admin_cluster }
it { expect(policy).to be_disallowed :read_cluster }
end
context 'when developer' do
before do
project.add_developer(user)
end
it { expect(policy).to be_disallowed :admin_cluster }
it { expect(policy).to be_disallowed :read_cluster }
it { expect(policy).to be_allowed :read_cluster }
end
context 'when maintainer' do
......
......@@ -10,13 +10,22 @@ RSpec.describe Clusters::Agents::ActivityEventPolicy do
let(:project) { event.agent.project }
describe 'rules' do
context 'reporter' do
before do
project.add_reporter(user)
end
it { expect(policy).to be_disallowed :admin_cluster }
it { expect(policy).to be_disallowed :read_cluster }
end
context 'developer' do
before do
project.add_developer(user)
end
it { expect(policy).to be_disallowed :admin_cluster }
it { expect(policy).to be_disallowed :read_cluster }
it { expect(policy).to be_allowed :read_cluster }
end
context 'maintainer' do
......
......@@ -79,6 +79,30 @@ RSpec.describe ClusterablePresenter do
end
end
describe '#can_admin_cluster?' do
let(:user) { create(:user) }
subject { described_class.new(clusterable).can_admin_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
describe '#environments_cluster_path' do
subject { described_class.new(clusterable).environments_cluster_path(cluster) }
......
......@@ -6,11 +6,11 @@ RSpec.describe API::GroupClusters do
include KubernetesHelpers
let(:current_user) { create(:user) }
let(:developer_user) { create(:user) }
let(:unauthorized_user) { create(:user) }
let(:group) { create(:group, :private) }
before do
group.add_developer(developer_user)
group.add_reporter(unauthorized_user)
group.add_maintainer(current_user)
end
......@@ -24,7 +24,7 @@ RSpec.describe API::GroupClusters do
context 'non-authorized user' do
it 'responds with 403' do
get api("/groups/#{group.id}/clusters", developer_user)
get api("/groups/#{group.id}/clusters", unauthorized_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
......@@ -68,7 +68,7 @@ RSpec.describe API::GroupClusters do
context 'non-authorized user' do
it 'responds with 403' do
get api("/groups/#{group.id}/clusters/#{cluster_id}", developer_user)
get api("/groups/#{group.id}/clusters/#{cluster_id}", unauthorized_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
......@@ -183,7 +183,7 @@ RSpec.describe API::GroupClusters do
context 'non-authorized user' do
it 'responds with 403' do
post api("/groups/#{group.id}/clusters/user", developer_user), params: cluster_params
post api("/groups/#{group.id}/clusters/user", unauthorized_user), params: cluster_params
expect(response).to have_gitlab_http_status(:forbidden)
end
......@@ -290,7 +290,7 @@ RSpec.describe API::GroupClusters do
context 'non-authorized user' do
before do
post api("/groups/#{group.id}/clusters/user", developer_user), params: cluster_params
post api("/groups/#{group.id}/clusters/user", unauthorized_user), params: cluster_params
end
it 'responds with 403' do
......@@ -364,7 +364,7 @@ RSpec.describe API::GroupClusters do
context 'non-authorized user' do
it 'responds with 403' do
put api("/groups/#{group.id}/clusters/#{cluster.id}", developer_user), params: update_params
put api("/groups/#{group.id}/clusters/#{cluster.id}", unauthorized_user), params: update_params
expect(response).to have_gitlab_http_status(:forbidden)
end
......@@ -505,7 +505,7 @@ RSpec.describe API::GroupClusters do
context 'non-authorized user' do
it 'responds with 403' do
delete api("/groups/#{group.id}/clusters/#{cluster.id}", developer_user), params: cluster_params
delete api("/groups/#{group.id}/clusters/#{cluster.id}", unauthorized_user), params: cluster_params
expect(response).to have_gitlab_http_status(:forbidden)
end
......
......@@ -5,13 +5,15 @@ require 'spec_helper'
RSpec.describe API::ProjectClusters do
include KubernetesHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:maintainer_user) { create(:user) }
let_it_be(:developer_user) { create(:user) }
let_it_be(:reporter_user) { create(:user) }
let_it_be(:project) { create(:project) }
before do
project.add_maintainer(current_user)
project.add_maintainer(maintainer_user)
project.add_developer(developer_user)
project.add_reporter(reporter_user)
end
describe 'GET /projects/:id/clusters' do
......@@ -24,7 +26,7 @@ RSpec.describe API::ProjectClusters do
context 'non-authorized user' do
it 'responds with 403' do
get api("/projects/#{project.id}/clusters", developer_user)
get api("/projects/#{project.id}/clusters", reporter_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
......@@ -32,7 +34,7 @@ RSpec.describe API::ProjectClusters do
context 'authorized user' do
before do
get api("/projects/#{project.id}/clusters", current_user)
get api("/projects/#{project.id}/clusters", developer_user)
end
it 'includes pagination headers' do
......@@ -61,13 +63,13 @@ RSpec.describe API::ProjectClusters do
let(:cluster) do
create(:cluster, :project, :provided_by_gcp, :with_domain,
platform_kubernetes: platform_kubernetes,
user: current_user,
user: maintainer_user,
projects: [project])
end
context 'non-authorized user' do
it 'responds with 403' do
get api("/projects/#{project.id}/clusters/#{cluster_id}", developer_user)
get api("/projects/#{project.id}/clusters/#{cluster_id}", reporter_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
......@@ -75,7 +77,7 @@ RSpec.describe API::ProjectClusters do
context 'authorized user' do
before do
get api("/projects/#{project.id}/clusters/#{cluster_id}", current_user)
get api("/projects/#{project.id}/clusters/#{cluster_id}", developer_user)
end
it 'returns specific cluster' do
......@@ -111,8 +113,8 @@ RSpec.describe API::ProjectClusters do
it 'returns user information' do
user = json_response['user']
expect(user['id']).to eq(current_user.id)
expect(user['username']).to eq(current_user.username)
expect(user['id']).to eq(maintainer_user.id)
expect(user['username']).to eq(maintainer_user.username)
end
it 'returns GCP provider information' do
......@@ -156,7 +158,7 @@ RSpec.describe API::ProjectClusters do
let(:management_project_id) { management_project.id }
before do
management_project.add_maintainer(current_user)
management_project.add_maintainer(maintainer_user)
end
let(:platform_kubernetes_attributes) do
......@@ -190,7 +192,7 @@ RSpec.describe API::ProjectClusters do
context 'authorized user' do
before do
post api("/projects/#{project.id}/clusters/user", current_user), params: cluster_params
post api("/projects/#{project.id}/clusters/user", maintainer_user), params: cluster_params
end
context 'with valid params' do
......@@ -317,7 +319,7 @@ RSpec.describe API::ProjectClusters do
create(:cluster, :provided_by_gcp, :project,
projects: [project])
post api("/projects/#{project.id}/clusters/user", current_user), params: cluster_params
post api("/projects/#{project.id}/clusters/user", maintainer_user), params: cluster_params
end
it 'responds with 201' do
......@@ -369,9 +371,9 @@ RSpec.describe API::ProjectClusters do
context 'authorized user' do
before do
management_project.add_maintainer(current_user)
management_project.add_maintainer(maintainer_user)
put api("/projects/#{project.id}/clusters/#{cluster.id}", current_user), params: update_params
put api("/projects/#{project.id}/clusters/#{cluster.id}", maintainer_user), params: update_params
cluster.reload
end
......@@ -501,7 +503,7 @@ RSpec.describe API::ProjectClusters do
context 'authorized user' do
before do
delete api("/projects/#{project.id}/clusters/#{cluster.id}", current_user), params: cluster_params
delete api("/projects/#{project.id}/clusters/#{cluster.id}", maintainer_user), params: cluster_params
end
it 'deletes the cluster' do
......
......@@ -14,7 +14,7 @@ RSpec.describe Projects::ClusterAgentsController do
let_it_be(:user) { create(:user) }
before do
project.add_developer(user)
project.add_reporter(user)
sign_in(user)
subject
end
......
......@@ -7,7 +7,7 @@ RSpec.describe DeploymentClusterEntity do
subject { described_class.new(deployment, request: request).as_json }
let(:maintainer) { create(:user) }
let(:developer) { create(:user) }
let(:reporter) { create(:user) }
let(:current_user) { maintainer }
let(:request) { double(:request, current_user: current_user) }
let(:project) { create(:project) }
......@@ -17,7 +17,7 @@ RSpec.describe DeploymentClusterEntity do
before do
project.add_maintainer(maintainer)
project.add_developer(developer)
project.add_reporter(reporter)
end
it 'matches deployment_cluster entity schema' do
......@@ -31,7 +31,7 @@ RSpec.describe DeploymentClusterEntity do
end
context 'when the user does not have permission to view the cluster' do
let(:current_user) { developer }
let(:current_user) { reporter }
it 'does not include the path nor the namespace' do
expect(subject[:path]).to be_nil
......
......@@ -47,6 +47,7 @@ RSpec.shared_context 'GroupPolicy context' do
create_custom_emoji
create_package
create_package_settings
read_cluster
]
end
......@@ -54,7 +55,7 @@ RSpec.shared_context 'GroupPolicy context' do
%i[
destroy_package
create_projects
read_cluster create_cluster update_cluster admin_cluster add_cluster
create_cluster update_cluster admin_cluster add_cluster
]
end
......
......@@ -6,12 +6,24 @@ RSpec.shared_examples 'clusterable policies' do
subject { described_class.new(current_user, clusterable) }
context 'with a reporter' do
before do
clusterable.add_reporter(current_user)
end
it { expect_disallowed(:read_cluster) }
it { expect_disallowed(:add_cluster) }
it { expect_disallowed(:create_cluster) }
it { expect_disallowed(:update_cluster) }
it { expect_disallowed(:admin_cluster) }
end
context 'with a developer' do
before do
clusterable.add_developer(current_user)
end
it { expect_disallowed(:read_cluster) }
it { expect_allowed(:read_cluster) }
it { expect_disallowed(:add_cluster) }
it { expect_disallowed(:create_cluster) }
it { expect_disallowed(:update_cluster) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment