PSA minor changes (#8022)

* Add missing and correct i18n PSA labels

* Correct description size by removing helper

* Add title to Namespace list tooltip if any PSA

* Allow to disable use of checkbox for PSA form

* Change timeout for growl to 5s on Pod warning due PSA

* Add type label for PSA

* Move PSA menu under Advanced

* Replace toggling system label with extending the value within the same

* Emit initial PSA form values con creation if no checkboxes due lack of interactions
This commit is contained in:
Giuseppe Leo 2023-01-25 14:02:26 +01:00 committed by GitHub
parent 33e428b945
commit 0768d82f16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 202 additions and 28 deletions

View File

@ -3942,10 +3942,20 @@ plugins:
title: Remove the Rancher Extensions Custom Resource Definition title: Remove the Rancher Extensions Custom Resource Definition
prompt: There are one or more extensions installed - removing the CRD will require you to manually reinstall these extensions if you subsequently re-enable extensions support. prompt: There are one or more extensions installed - removing the CRD will require you to manually reinstall these extensions if you subsequently re-enable extensions support.
podSecurityAdmission: podSecurityAdmission:
label: Pod Security Admission name: Pod Security Admission
description: Define the admission control mode you want to use for the pod security description: Define the admission control mode you want to use for the pod security
banner: banner:
modifications: 'Note: Modifying a Pod Security Admission Configuration Template will not affect any downstream cluster that references it until there is a cluster update' modifications: 'Note: Modifying a Pod Security Admission Configuration Template will not affect any downstream cluster that references it until there is a cluster update'
labels:
enforce: Enforce
audit: Audit
warn: Warn
usernames: Usernames
runtimeClasses: RuntimeClasses
namespaces: Namespaces
privileged: privileged
baseline: baseline
restricted: restricted
version: version:
placeholder: 'Version (default: latest)' placeholder: 'Version (default: latest)'
exemptions: exemptions:
@ -6213,6 +6223,11 @@ typeLabel:
one { Workspace } one { Workspace }
other { Workspaces } other { Workspaces }
} }
management.cattle.io.podsecurityadmissionconfigurationtemplate: |-
{count, plural,
one { Pod Security Admission }
other { Pod Security Admissions }
}
policy.poddisruptionbudget: |- policy.poddisruptionbudget: |-
{count, plural, {count, plural,
one { Pod Disruption Budget } one { Pod Disruption Budget }

View File

@ -90,7 +90,7 @@ export default {
}, },
labels() { labels() {
if (this.showAllLabels || !this.showFilteredSystemLabels) { if (!this.showFilteredSystemLabels) {
return this.value?.labels || {}; return this.value?.labels || {};
} }
@ -249,6 +249,7 @@ export default {
v-tooltip="prop ? `${key} : ${prop}` : key" v-tooltip="prop ? `${key} : ${prop}` : key"
> >
<span>{{ internalTooltips[key] ? internalTooltips[key] : key }}</span> <span>{{ internalTooltips[key] ? internalTooltips[key] : key }}</span>
<span v-if="showAllLabels">: {{ key }}</span>
</span> </span>
<span v-else>{{ prop ? `${key} : ${prop}` : key }}</span> <span v-else>{{ prop ? `${key} : ${prop}` : key }}</span>
</Tag> </Tag>

View File

@ -235,13 +235,17 @@ export default {
} }
}, },
methods: { methods: {
getPSA(row) { /**
* Get PSA HTML to be displayed in the tooltips
*/
getPsaTooltip(row) {
const dictionary = row.psaTooltipsDescription; const dictionary = row.psaTooltipsDescription;
const list = Object.values(dictionary) const list = Object.values(dictionary)
.sort() .sort()
.map(text => `<li>${ text }</li>`).join(''); .map(text => `<li>${ text }</li>`).join('');
const title = `<p>${ this.t('podSecurityAdmission.name') }: </p>`;
return `<ul class="psa-tooltip">${ list }</ul>`; return `${ title }<ul class="psa-tooltip">${ list }</ul>`;
}, },
userIsFilteringForSpecificNamespaceOrProject() { userIsFilteringForSpecificNamespaceOrProject() {
@ -409,7 +413,7 @@ export default {
</span> </span>
<i <i
v-if="row.hasSystemLabels" v-if="row.hasSystemLabels"
v-tooltip="getPSA(row)" v-tooltip="getPsaTooltip(row)"
class="icon icon-lock ml-5" class="icon icon-lock ml-5"
/> />
</div> </div>

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { _VIEW } from '@shell/config/query-params'; import { _VIEW, _CREATE } from '@shell/config/query-params';
import LabeledSelect from '@shell/components/form/LabeledSelect.vue'; import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import Checkbox from '@components/Form/Checkbox/Checkbox.vue'; import Checkbox from '@components/Form/Checkbox/Checkbox.vue';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue'; import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
@ -39,6 +39,11 @@ export default Vue.extend({
default: () => ({}) default: () => ({})
}, },
labelsAlwaysActive: {
type: Boolean,
default: false
},
/** /**
* Map editing capabilities to the component * Map editing capabilities to the component
*/ */
@ -78,7 +83,10 @@ export default Vue.extend({
// Generate PSA form controls // Generate PSA form controls
psaControls: toDictionary(PSAModes, getPsaControl) as Record<PSAMode, PSAControl>, psaControls: toDictionary(PSAModes, getPsaControl) as Record<PSAMode, PSAControl>,
psaExemptionsControls: toDictionary(PSADimensions, getExemptionControl) as Record<PSADimension, PSAExemptionControl>, psaExemptionsControls: toDictionary(PSADimensions, getExemptionControl) as Record<PSADimension, PSAExemptionControl>,
options: PSALevels, options: PSALevels.map(level => ({
value: level,
label: this.t(`podSecurityAdmission.labels.${ level }`)
})),
}; };
}, },
@ -105,6 +113,12 @@ export default Vue.extend({
}; };
this.psaExemptionsControls = this.getPsaExemptions(); this.psaExemptionsControls = this.getPsaExemptions();
// Emit initial value on creation if labels always active, as default predefined values are required
if (this.mode === _CREATE && this.labelsAlwaysActive) {
this.updateLabels();
this.updateExemptions();
}
}, },
methods: { methods: {
@ -114,7 +128,7 @@ export default Vue.extend({
updateLabels(): void { updateLabels(): void {
const nonPSALabels = pickBy(this.labels, (_, key) => !key.includes(this.labelsPrefix)); const nonPSALabels = pickBy(this.labels, (_, key) => !key.includes(this.labelsPrefix));
const labels = PSAModes.reduce((acc, mode) => { const labels = PSAModes.reduce((acc, mode) => {
return this.psaControls[mode].active ? { return this.psaControls[mode].active || this.labelsAlwaysActive ? {
...acc, ...acc,
// Set default level if none // Set default level if none
[`${ this.labelsPrefix }${ mode }`]: this.psaControls[mode].level || PSADefaultLevel, [`${ this.labelsPrefix }${ mode }`]: this.psaControls[mode].level || PSADefaultLevel,
@ -126,6 +140,9 @@ export default Vue.extend({
this.$emit('updateLabels', labels); this.$emit('updateLabels', labels);
}, },
/**
* Emit active exemptions in required format
*/
updateExemptions(): void { updateExemptions(): void {
const exemptions = PSADimensions.reduce((acc, dimension) => { const exemptions = PSADimensions.reduce((acc, dimension) => {
const value = this.psaExemptionsControls[dimension].value.split(',').map(value => value.trim()); const value = this.psaExemptionsControls[dimension].value.split(',').map(value => value.trim());
@ -175,6 +192,13 @@ export default Vue.extend({
} }
}; };
}, {}) as Record<PSADimension, PSAExemptionControl>; }, {}) as Record<PSADimension, PSAExemptionControl>;
},
/**
* Add checks on input for PSA controls to be active or not, allowing white cases
*/
isPsaControlDisabled(active: boolean): boolean {
return !this.labelsAlwaysActive && (!active || this.isView);
} }
} }
}); });
@ -183,7 +207,7 @@ export default Vue.extend({
<template> <template>
<div class="psa"> <div class="psa">
<!-- PSA --> <!-- PSA -->
<p class="helper-text mb-30"> <p class="mb-30">
<t k="podSecurityAdmission.description" /> <t k="podSecurityAdmission.description" />
</p> </p>
@ -194,19 +218,28 @@ export default Vue.extend({
> >
<span class="col span-2"> <span class="col span-2">
<Checkbox <Checkbox
v-if="!labelsAlwaysActive"
v-model="psaControl.active" v-model="psaControl.active"
:data-testid="componentTestid + '--psaControl-' + i + '-active'" :data-testid="componentTestid + '--psaControl-' + i + '-active'"
:label="level" :label="level"
:label-key="`podSecurityAdmission.labels.${ level }`"
:disabled="isView" :disabled="isView"
@input="updateLabels()" @input="updateLabels()"
/> />
<p v-else>
<t :k="`podSecurityAdmission.labels.${level}`" />
</p>
</span> </span>
<span class="col span-4"> <span
class="
col
span-4"
>
<LabeledSelect <LabeledSelect
v-model="psaControl.level" v-model="psaControl.level"
:data-testid="componentTestid + '--psaControl-' + i + '-level'" :data-testid="componentTestid + '--psaControl-' + i + '-level'"
:disabled="(isView || !psaControl.active)" :disabled="isPsaControlDisabled(psaControl.active)"
:options="options" :options="options"
:mode="mode" :mode="mode"
@input="updateLabels()" @input="updateLabels()"
@ -217,7 +250,7 @@ export default Vue.extend({
<LabeledInput <LabeledInput
v-model="psaControl.version" v-model="psaControl.version"
:data-testid="componentTestid + '--psaControl-' + i + '-version'" :data-testid="componentTestid + '--psaControl-' + i + '-version'"
:disabled="(isView || !psaControl.active)" :disabled="isPsaControlDisabled(psaControl.active)"
:options="options" :options="options"
:placeholder="t('podSecurityAdmission.version.placeholder', { psaControl: mode })" :placeholder="t('podSecurityAdmission.version.placeholder', { psaControl: mode })"
:mode="mode" :mode="mode"
@ -233,7 +266,7 @@ export default Vue.extend({
<t k="podSecurityAdmission.exemptions.title" /> <t k="podSecurityAdmission.exemptions.title" />
</h3> </h3>
</slot> </slot>
<p class="helper-text mb-30"> <p class="mb-30">
<t k="podSecurityAdmission.exemptions.description" /> <t k="podSecurityAdmission.exemptions.description" />
</p> </p>
@ -247,6 +280,7 @@ export default Vue.extend({
v-model="psaExemptionsControl.active" v-model="psaExemptionsControl.active"
:data-testid="componentTestid + '--psaExemptionsControl-' + i + '-active'" :data-testid="componentTestid + '--psaExemptionsControl-' + i + '-active'"
:label="dimension" :label="dimension"
:label-key="`podSecurityAdmission.labels.${ dimension }`"
:disabled="isView" :disabled="isView"
@input="updateExemptions()" @input="updateExemptions()"
/> />

View File

@ -2,13 +2,31 @@ import { mount } from '@vue/test-utils';
import PodSecurityAdmission from '@shell/components/PodSecurityAdmission.vue'; import PodSecurityAdmission from '@shell/components/PodSecurityAdmission.vue';
describe('component: PodSecurityAdmission', () => { describe('component: PodSecurityAdmission', () => {
it.each([
['updateLabels', {
audit: 'privileged',
'audit-version': 'latest',
enforce: 'privileged',
'enforce-version': 'latest',
warn: 'privileged',
'warn-version': 'latest',
}],
['updateExemptions', {
namespaces: [], runtimeClasses: [], usernames: []
}]
])('should emit %p and exemptions on creation if labels always active', (emission, value) => {
const wrapper = mount(PodSecurityAdmission, { propsData: { mode: 'create', labelsAlwaysActive: true } });
expect(wrapper.emitted(emission)![0][0]).toStrictEqual(value);
});
describe('handling labels', () => { describe('handling labels', () => {
it.each([ it.each([
['true', 'active'], ['true', 'active'],
['', 'level'], ['', 'level'],
['', 'version'], ['', 'version'],
])('should display default value %p for input %p', (value, inputId) => { ])('should display default value %p for input %p', (value, inputId) => {
const wrapper = mount(PodSecurityAdmission, { propsData: { mode: 'create' } }); const wrapper = mount(PodSecurityAdmission, { propsData: { mode: 'edit' } });
const input = wrapper.find(`[data-testid="pod-security-admission--psaControl-0-${ inputId }"]`).find('input').element as HTMLInputElement; const input = wrapper.find(`[data-testid="pod-security-admission--psaControl-0-${ inputId }"]`).find('input').element as HTMLInputElement;
@ -44,7 +62,7 @@ describe('component: PodSecurityAdmission', () => {
const wrapper = mount(PodSecurityAdmission, { const wrapper = mount(PodSecurityAdmission, {
propsData: { propsData: {
mode: 'create', mode: 'edit',
labels, labels,
labelsPrefix: prefix labelsPrefix: prefix
} }
@ -65,7 +83,7 @@ describe('component: PodSecurityAdmission', () => {
const wrapper = mount(PodSecurityAdmission, { const wrapper = mount(PodSecurityAdmission, {
propsData: { propsData: {
mode: 'create', mode: 'edit',
labels, labels,
labelsPrefix: prefix labelsPrefix: prefix
} }
@ -88,7 +106,7 @@ describe('component: PodSecurityAdmission', () => {
const wrapper = mount(PodSecurityAdmission, { const wrapper = mount(PodSecurityAdmission, {
propsData: { propsData: {
mode: 'create', mode: 'edit',
labels, labels,
labelsPrefix: prefix labelsPrefix: prefix
} }
@ -108,7 +126,7 @@ describe('component: PodSecurityAdmission', () => {
}; };
const wrapper = mount(PodSecurityAdmission, { const wrapper = mount(PodSecurityAdmission, {
propsData: { propsData: {
mode: 'create', mode: 'edit',
labels: {}, labels: {},
labelsPrefix: prefix labelsPrefix: prefix
}, },
@ -141,7 +159,7 @@ describe('component: PodSecurityAdmission', () => {
}; };
const wrapper = mount(PodSecurityAdmission, { const wrapper = mount(PodSecurityAdmission, {
propsData: { propsData: {
mode: 'create', mode: 'edit',
labels, labels,
labelsPrefix: prefix labelsPrefix: prefix
}, },
@ -177,7 +195,7 @@ describe('component: PodSecurityAdmission', () => {
it('should assign default version and level if missing', () => { it('should assign default version and level if missing', () => {
const wrapper = mount(PodSecurityAdmission, { const wrapper = mount(PodSecurityAdmission, {
propsData: { propsData: {
mode: 'create', mode: 'edit',
labels: {}, labels: {},
labelsPrefix: prefix labelsPrefix: prefix
}, },
@ -204,6 +222,104 @@ describe('component: PodSecurityAdmission', () => {
}); });
}); });
}); });
describe.each(['level', 'version'])('should keep always %p enabled', (inputId) => {
it('given labelsAlwaysActive true and no labels', () => {
const wrapper = mount(PodSecurityAdmission, {
propsData: {
mode: 'edit',
labelsAlwaysActive: true,
labels: {}
},
});
const input = wrapper.find(`[data-testid="pod-security-admission--psaControl-0-${ inputId }"]`).find('input').element as HTMLInputElement;
expect(input.disabled).toBe(false);
});
it('given existing values', () => {
const wrapper = mount(PodSecurityAdmission, {
propsData: {
mode: 'edit',
labels: {
[`enforce`]: 'baseline',
[`enforce-version`]: '123'
}
},
});
const input = wrapper.find(`[data-testid="pod-security-admission--psaControl-0-${ inputId }"]`).find('input').element as HTMLInputElement;
expect(input.disabled).toBe(false);
});
});
describe.each(['level', 'version'])('should keep always %p disabled', (inputId) => {
it('given labelsAlwaysActive false and no labels', () => {
const wrapper = mount(PodSecurityAdmission, {
propsData: {
mode: 'edit',
labelsAlwaysActive: false,
labels: {}
},
});
const input = wrapper.find(`[data-testid="pod-security-admission--psaControl-0-${ inputId }"]`).find('input').element as HTMLInputElement;
expect(input.disabled).toBe(true);
});
it('given disabled active status', () => {
const wrapper = mount(PodSecurityAdmission, {
propsData: { mode: 'edit' },
data: () => ({
psaControls: {
enforce: {
active: false,
level: '',
version: ''
}
}
}),
});
const input = wrapper.find(`[data-testid="pod-security-admission--psaControl-0-${ inputId }"]`).find('input').element as HTMLInputElement;
expect(input.disabled).toBe(true);
});
it('given view mode and provided labels', () => {
const wrapper = mount(PodSecurityAdmission, {
propsData: {
mode: 'view',
labels: {
[`enforce`]: 'baseline',
[`enforce-version`]: '123'
}
},
});
const input = wrapper.find(`[data-testid="pod-security-admission--psaControl-0-${ inputId }"]`).find('input').element as HTMLInputElement;
expect(input.disabled).toBe(true);
});
});
it.each([
[true, false],
])('should display the checkbox %p', (value) => {
const wrapper = mount(PodSecurityAdmission, {
propsData: {
mode: 'edit',
labelsAlwaysActive: value
}
});
const input = wrapper.find(`[data-testid="pod-security-admission--psaControl-0-active"]`).element as HTMLInputElement;
expect(!input).toBe(value);
});
}); });
describe('handling exemptions', () => { describe('handling exemptions', () => {
@ -228,7 +344,7 @@ describe('component: PodSecurityAdmission', () => {
const wrapper = mount(PodSecurityAdmission, { const wrapper = mount(PodSecurityAdmission, {
propsData: { propsData: {
mode: 'create', mode: 'edit',
exemptions, exemptions,
} }
}); });
@ -243,7 +359,7 @@ describe('component: PodSecurityAdmission', () => {
const exemptions = { usernames: [value] }; const exemptions = { usernames: [value] };
const wrapper = mount(PodSecurityAdmission, { const wrapper = mount(PodSecurityAdmission, {
propsData: { propsData: {
mode: 'create', mode: 'edit',
exemptions exemptions
} }
}); });
@ -266,7 +382,7 @@ describe('component: PodSecurityAdmission', () => {
}; };
const wrapper = mount(PodSecurityAdmission, { const wrapper = mount(PodSecurityAdmission, {
propsData: { propsData: {
mode: 'create', mode: 'edit',
exemptions exemptions
}, },
}); });

View File

@ -75,7 +75,7 @@ export default {
:on-label="t('labels.labels.show')" :on-label="t('labels.labels.show')"
/> />
</div> </div>
<p class="helper-text mt-10 mb-10"> <p class="mt-10 mb-10">
<t k="labels.labels.description" /> <t k="labels.labels.description" />
</p> </p>
<div :class="sectionClass"> <div :class="sectionClass">

View File

@ -3,7 +3,8 @@ import {
CAPI, CAPI,
CATALOG, CATALOG,
NORMAN, NORMAN,
HCI HCI,
MANAGEMENT
} from '@shell/config/types'; } from '@shell/config/types';
import { MULTI_CLUSTER } from '@shell/store/features'; import { MULTI_CLUSTER } from '@shell/store/features';
import { DSL } from '@shell/store/type-map'; import { DSL } from '@shell/store/type-map';
@ -65,7 +66,6 @@ export function init(store) {
'cloud-credentials', 'cloud-credentials',
'drivers', 'drivers',
'pod-security-policies', 'pod-security-policies',
'management.cattle.io.podsecurityadmissionconfigurationtemplate'
]); ]);
configureType(CAPI.RANCHER_CLUSTER, { configureType(CAPI.RANCHER_CLUSTER, {
@ -120,6 +120,7 @@ export function init(store) {
weightType(CAPI.MACHINE_SET, 2, true); weightType(CAPI.MACHINE_SET, 2, true);
weightType(CAPI.MACHINE, 1, true); weightType(CAPI.MACHINE, 1, true);
weightType(CATALOG.CLUSTER_REPO, 0, true); weightType(CATALOG.CLUSTER_REPO, 0, true);
weightType(MANAGEMENT.PSA, 5, true);
basicType([ basicType([
CAPI.MACHINE_DEPLOYMENT, CAPI.MACHINE_DEPLOYMENT,
@ -127,6 +128,7 @@ export function init(store) {
CAPI.MACHINE, CAPI.MACHINE,
CATALOG.CLUSTER_REPO, CATALOG.CLUSTER_REPO,
'pod-security-policies', 'pod-security-policies',
MANAGEMENT.PSA
], 'advanced'); ], 'advanced');
weightGroup('advanced', -1, true); weightGroup('advanced', -1, true);

View File

@ -97,6 +97,7 @@ export default (Vue as VueConstructor<Vue & InstanceType<typeof CreateEditView>>
/> />
<PodSecurityAdmission <PodSecurityAdmission
:labels="defaults" :labels="defaults"
:labels-always-active="true"
:exemptions="exemptions" :exemptions="exemptions"
:mode="mode" :mode="mode"
@updateLabels="setDefaults($event)" @updateLabels="setDefaults($event)"

View File

@ -238,8 +238,8 @@ export default {
</Tab> </Tab>
<Tab <Tab
name="pod-security-admission" name="pod-security-admission"
label-key="podSecurityAdmission.label" label-key="podSecurityAdmission.name"
:label="t('podSecurityAdmission.label')" :label="t('podSecurityAdmission.name')"
> >
<PodSecurityAdmission <PodSecurityAdmission
:labels="value.labels" :labels="value.labels"

View File

@ -190,6 +190,7 @@ export default class Pod extends WorkloadService {
this.$dispatch('growl/warning', { this.$dispatch('growl/warning', {
title: this.$rootGetters['i18n/t']('growl.podSecurity.title'), title: this.$rootGetters['i18n/t']('growl.podSecurity.title'),
message: this.$rootGetters['i18n/t']('growl.podSecurity.message'), message: this.$rootGetters['i18n/t']('growl.podSecurity.message'),
timeout: 5000,
}, { root: true }); }, { root: true });
} }
} }