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