mirror of https://github.com/rancher/dashboard.git
514 lines
14 KiB
Vue
514 lines
14 KiB
Vue
<script>
|
|
import { MANAGEMENT } from '@/config/types';
|
|
import CruResource from '@/components/CruResource';
|
|
import CreateEditView from '@/mixins/create-edit-view';
|
|
import RadioGroup from '@/components/form/RadioGroup';
|
|
import Select from '@/components/form/Select';
|
|
import LabeledInput from '@/components/form/LabeledInput';
|
|
import ArrayList from '@/components/form/ArrayList';
|
|
import NameNsDescription from '@/components/form/NameNsDescription';
|
|
import Tab from '@/components/Tabbed/Tab';
|
|
import Tabbed from '@/components/Tabbed';
|
|
import { ucFirst } from '@/utils/string';
|
|
import SortableTable from '@/components/SortableTable';
|
|
import { _DETAIL } from '@/config/query-params';
|
|
import { SUBTYPE_MAPPING, VERBS } from '@/models/management.cattle.io.roletemplate';
|
|
import Loading from '@/components/Loading';
|
|
import capitalize from 'lodash/capitalize';
|
|
|
|
const GLOBAL = SUBTYPE_MAPPING.GLOBAL.key;
|
|
const CLUSTER = SUBTYPE_MAPPING.CLUSTER.key;
|
|
const NAMESPACE = SUBTYPE_MAPPING.NAMESPACE.key;
|
|
const RBAC_ROLE = SUBTYPE_MAPPING.RBAC_ROLE.key;
|
|
|
|
/**
|
|
* Handles the View, Create and Edit of
|
|
* - management.cattle.io.globalrole
|
|
* - management.cattle.io.roletemplate
|
|
* - rbac.authorization.k8s.io.role
|
|
* - rbac.authorization.k8s.io.clusterrole
|
|
*
|
|
* management.cattle.io.roletemplate is further split into two types
|
|
* - Cluster
|
|
* - Project/Namespace
|
|
*
|
|
* The above means there are 4 types ==> 5 subtypes handled by this component
|
|
*
|
|
*/
|
|
export default {
|
|
components: {
|
|
ArrayList,
|
|
CruResource,
|
|
LabeledInput,
|
|
RadioGroup,
|
|
Select,
|
|
NameNsDescription,
|
|
Tab,
|
|
Tabbed,
|
|
SortableTable,
|
|
Loading,
|
|
},
|
|
|
|
mixins: [CreateEditView],
|
|
|
|
async fetch() {
|
|
if (this.value.subtype === CLUSTER || this.value.subtype === NAMESPACE) {
|
|
this.templateOptions = (await this.$store.dispatch(`management/findAll`, { type: MANAGEMENT.ROLE_TEMPLATE }))
|
|
.map(option => ({
|
|
label: option.nameDisplay,
|
|
value: option.id
|
|
}));
|
|
}
|
|
},
|
|
|
|
data() {
|
|
this.$set(this.value, 'rules', this.value.rules || []);
|
|
|
|
this.value.rules.forEach((rule) => {
|
|
if (rule.verbs[0] === '*') {
|
|
this.$set(rule, 'verbs', [...VERBS]);
|
|
}
|
|
});
|
|
|
|
const query = { ...this.$route.query };
|
|
const { roleContext } = query;
|
|
|
|
if (roleContext && this.value.updateSubtype) {
|
|
this.value.updateSubtype(roleContext);
|
|
}
|
|
|
|
switch (this.value.subtype) {
|
|
case GLOBAL:
|
|
this.$set(this.value, 'newUserDefault', !!this.value.newUserDefault);
|
|
break;
|
|
case CLUSTER:
|
|
case NAMESPACE:
|
|
this.$set(this.value, 'roleTemplateNames', this.value.roleTemplateNames || []);
|
|
this.$set(this.value, 'locked', !!this.value.locked);
|
|
break;
|
|
}
|
|
|
|
this.$nextTick(() => {
|
|
this.$emit('set-subtype', this.label);
|
|
});
|
|
|
|
return {
|
|
defaultRule: {
|
|
apiGroups: [''],
|
|
nonResourceURLs: [],
|
|
resourceNames: [],
|
|
resources: [],
|
|
verbs: []
|
|
},
|
|
verbOptions: VERBS,
|
|
templateOptions: []
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
label() {
|
|
return this.t(`rbac.roletemplate.subtypes.${ this.value.subtype }.label`);
|
|
},
|
|
defaultLabel() {
|
|
return this.t(`rbac.roletemplate.subtypes.${ this.value.subtype }.defaultLabel`);
|
|
},
|
|
lockedOptions() {
|
|
return [
|
|
{
|
|
value: true,
|
|
label: this.t('rbac.roletemplate.locked.yes')
|
|
},
|
|
{
|
|
value: false,
|
|
label: this.t('rbac.roletemplate.locked.no')
|
|
}
|
|
];
|
|
},
|
|
resourceOptions() {
|
|
return this.value.resources.map(resource => ({
|
|
value: resource.toLowerCase(),
|
|
label: capitalize(resource)
|
|
}));
|
|
},
|
|
newUserDefaultOptions() {
|
|
return [
|
|
{
|
|
value: true,
|
|
label: this.t(`rbac.roletemplate.subtypes.${ this.value.subtype }.yes`)
|
|
},
|
|
{
|
|
value: false,
|
|
label: this.t('rbac.roletemplate.newUserDefault.no')
|
|
}
|
|
];
|
|
},
|
|
isRancherRoleTemplate() {
|
|
return this.value.subtype === CLUSTER || this.value.subtype === NAMESPACE;
|
|
},
|
|
isNamespaced() {
|
|
return this.value.subtype === RBAC_ROLE;
|
|
},
|
|
isRancherType() {
|
|
return this.value.subtype === GLOBAL || this.value.subtype === CLUSTER || this.value.subtype === NAMESPACE;
|
|
},
|
|
isDetail() {
|
|
return this.as === _DETAIL;
|
|
},
|
|
doneLocationOverride() {
|
|
return this.value.listLocation;
|
|
},
|
|
ruleClass() {
|
|
return `col ${ this.isNamespaced ? 'span-4' : 'span-3' }`;
|
|
},
|
|
// Detail View
|
|
rules() {
|
|
return this.createRules(this.value);
|
|
},
|
|
ruleHeaders() {
|
|
const verbHeaders = VERBS.map(verb => ({
|
|
name: verb,
|
|
key: ucFirst(verb),
|
|
value: this.verbKey(verb),
|
|
formatter: 'Checked',
|
|
align: 'center'
|
|
}));
|
|
|
|
return [
|
|
...verbHeaders,
|
|
{
|
|
name: 'custom',
|
|
labelKey: 'tableHeaders.customVerbs',
|
|
key: ucFirst('custom'),
|
|
value: 'hasCustomVerbs',
|
|
formatter: 'Checked',
|
|
align: 'center'
|
|
},
|
|
{
|
|
name: 'resources',
|
|
labelKey: 'tableHeaders.resources',
|
|
value: 'resources',
|
|
formatter: 'list',
|
|
},
|
|
{
|
|
name: 'url',
|
|
labelKey: 'tableHeaders.url',
|
|
value: 'nonResourceURLs',
|
|
formatter: 'list',
|
|
},
|
|
{
|
|
name: 'apiGroups',
|
|
labelKey: 'tableHeaders.apiGroup',
|
|
value: 'apiGroups',
|
|
formatter: 'list',
|
|
}
|
|
];
|
|
},
|
|
inheritedRules() {
|
|
return this.createInheritedRules(this.value, [], false);
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
setRule(key, rule, event) {
|
|
const value = event.label ? event.label : event;
|
|
|
|
if (value || (key === 'apiGroups' && value === '')) {
|
|
this.$set(rule, key, [value]);
|
|
} else {
|
|
this.$set(rule, key, []);
|
|
}
|
|
},
|
|
getRule(key, rule) {
|
|
return rule[key]?.[0] || null;
|
|
},
|
|
updateSelectValue(row, key, event) {
|
|
const value = event.label ? event.value : event;
|
|
|
|
this.$set(row, key, value);
|
|
},
|
|
cancel() {
|
|
this.done();
|
|
},
|
|
async actuallySave(url) {
|
|
if ( this.isCreate ) {
|
|
url = url || this.schema.linkFor('collection');
|
|
await this.value.save({ url, redirectUnauthorized: false });
|
|
} else {
|
|
await this.value.save({ redirectUnauthorized: false });
|
|
}
|
|
},
|
|
// Detail View
|
|
verbKey(verb) {
|
|
return `has${ ucFirst(verb) }`;
|
|
},
|
|
createRules(role) {
|
|
return (role.rules || []).map((rule, i) => {
|
|
const tableRule = {
|
|
index: i,
|
|
apiGroups: rule.apiGroups || [''],
|
|
resources: rule.resources || [],
|
|
nonResourceURLs: rule.nonResourceURLs || []
|
|
};
|
|
|
|
VERBS.forEach((verb) => {
|
|
const key = this.verbKey(verb);
|
|
|
|
tableRule[key] = rule.verbs[0] === '*' || rule.verbs.includes(verb);
|
|
tableRule.hasCustomVerbs = rule.verbs.some(verb => !VERBS.includes(verb));
|
|
});
|
|
|
|
return tableRule;
|
|
});
|
|
},
|
|
createInheritedRules(parent, res = [], showParent = true) {
|
|
if (!parent.roleTemplateNames) {
|
|
return [];
|
|
}
|
|
|
|
parent.roleTemplateNames
|
|
.map(rtn => this.$store.getters[`management/byId`](MANAGEMENT.ROLE_TEMPLATE, rtn))
|
|
.forEach((rt) => {
|
|
// Add Self
|
|
res.push({
|
|
showParent,
|
|
parent,
|
|
template: rt,
|
|
rules: this.createRules(rt)
|
|
});
|
|
// Add inherited
|
|
this.createInheritedRules(rt, res);
|
|
});
|
|
|
|
return res;
|
|
},
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Loading v-if="$fetchState.pending" />
|
|
<CruResource
|
|
v-else
|
|
class="receiver"
|
|
:can-yaml="!isCreate"
|
|
:mode="mode"
|
|
:resource="value"
|
|
:errors="errors"
|
|
:cancel-event="true"
|
|
@error="e=>errors = e"
|
|
@finish="save"
|
|
@cancel="cancel"
|
|
>
|
|
<template v-if="isDetail">
|
|
<SortableTable
|
|
key-field="index"
|
|
:rows="rules"
|
|
:headers="ruleHeaders"
|
|
:table-actions="false"
|
|
:row-actions="false"
|
|
:search="false"
|
|
/>
|
|
<div v-for="(inherited, index) of inheritedRules" :key="index">
|
|
<div class="spacer"></div>
|
|
<h3>
|
|
Inherited from {{ inherited.template.nameDisplay }}
|
|
<template v-if="inherited.showParent">
|
|
{{ inherited.parent ? '(' + inherited.parent.nameDisplay + ')' : '' }}
|
|
</template>
|
|
</h3>
|
|
<SortableTable
|
|
key-field="index"
|
|
:rows="inherited.rules"
|
|
:headers="ruleHeaders"
|
|
:table-actions="false"
|
|
:row-actions="false"
|
|
:search="false"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<NameNsDescription
|
|
v-model="value"
|
|
:namespaced="isNamespaced"
|
|
:mode="mode"
|
|
name-key="displayName"
|
|
description-key="description"
|
|
label="Name"
|
|
/>
|
|
<div v-if="isRancherType" class="row">
|
|
<div class="col span-6">
|
|
<RadioGroup
|
|
:value="value.default"
|
|
name="storageSource"
|
|
:label="defaultLabel"
|
|
class="mb-10"
|
|
:options="newUserDefaultOptions"
|
|
:mode="mode"
|
|
@input="value.updateDefault"
|
|
/>
|
|
</div>
|
|
<div v-if="isRancherRoleTemplate" class="col span-6">
|
|
<RadioGroup
|
|
v-model="value.locked"
|
|
name="storageSource"
|
|
:label="t('rbac.roletemplate.locked.label')"
|
|
class="mb-10"
|
|
:options="lockedOptions"
|
|
:mode="mode"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="spacer"></div>
|
|
<Tabbed :side-tabs="true">
|
|
<Tab
|
|
name="grant-resources"
|
|
:label="t('rbac.roletemplate.tabs.grantResources.label')"
|
|
:weight="1"
|
|
>
|
|
<ArrayList
|
|
v-model="value.rules"
|
|
label="Resources"
|
|
:default-add-value="defaultRule"
|
|
:initial-empty-row="true"
|
|
:show-header="true"
|
|
add-label="Add Resource"
|
|
:mode="mode"
|
|
>
|
|
<template #column-headers>
|
|
<div class="column-headers row">
|
|
<div :class="ruleClass">
|
|
<label class="text-label">{{ t('rbac.roletemplate.tabs.grantResources.tableHeaders.verbs') }}
|
|
<span class="required">*</span>
|
|
</label>
|
|
</div>
|
|
<div :class="ruleClass">
|
|
<label class="text-label">{{ t('rbac.roletemplate.tabs.grantResources.tableHeaders.resources') }}
|
|
<span v-if="isNamespaced" class="required">*</span>
|
|
</label>
|
|
</div>
|
|
<div v-if="!isNamespaced" :class="ruleClass">
|
|
<label class="text-label">{{ t('rbac.roletemplate.tabs.grantResources.tableHeaders.nonResourceUrls') }}</label>
|
|
</div>
|
|
<div :class="ruleClass">
|
|
<label class="text-label">{{ t('rbac.roletemplate.tabs.grantResources.tableHeaders.apiGroups') }}
|
|
<span v-if="isNamespaced" class="required">*</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #columns="props">
|
|
<div class="columns row">
|
|
<div :class="ruleClass">
|
|
<Select
|
|
:value="props.row.value.verbs"
|
|
class="lg"
|
|
:taggable="true"
|
|
:searchable="true"
|
|
:options="verbOptions"
|
|
:multiple="true"
|
|
:mode="mode"
|
|
@input="updateSelectValue(props.row.value, 'verbs', $event)"
|
|
/>
|
|
</div>
|
|
<div :class="ruleClass">
|
|
<Select
|
|
:value="getRule('resources', props.row.value)"
|
|
:options="resourceOptions"
|
|
:searchable="true"
|
|
:taggable="true"
|
|
:mode="mode"
|
|
@input="setRule('resources', props.row.value, $event)"
|
|
/>
|
|
</div>
|
|
<div v-if="!isNamespaced" :class="ruleClass">
|
|
<LabeledInput
|
|
:value="getRule('nonResourceURLs', props.row.value)"
|
|
:mode="mode"
|
|
@input="setRule('nonResourceURLs', props.row.value, $event)"
|
|
/>
|
|
</div>
|
|
<div :class="ruleClass">
|
|
<LabeledInput
|
|
:value="getRule('apiGroups', props.row.value)"
|
|
:mode="mode"
|
|
@input="setRule('apiGroups', props.row.value, $event)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</ArrayList>
|
|
</Tab>
|
|
<Tab
|
|
v-if="isRancherRoleTemplate"
|
|
name="inherit-from"
|
|
label="Inherit From"
|
|
:weight="0"
|
|
>
|
|
<ArrayList
|
|
v-model="value.roleTemplateNames"
|
|
label="Resources"
|
|
add-label="Add Resource"
|
|
:mode="mode"
|
|
>
|
|
<template #columns="props">
|
|
<div class="columns row">
|
|
<div class="col span-12">
|
|
<Select
|
|
v-model="props.row.value"
|
|
class="lg"
|
|
:taggable="false"
|
|
:searchable="true"
|
|
:options="templateOptions"
|
|
option-key="value"
|
|
option-label="label"
|
|
:mode="mode"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</ArrayList>
|
|
</Tab>
|
|
</Tabbed>
|
|
</template>
|
|
</CruResource>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.required {
|
|
color: var(--error);
|
|
}
|
|
|
|
::v-deep {
|
|
.column-headers {
|
|
margin-right: 75px;
|
|
}
|
|
|
|
.box {
|
|
align-items: initial;
|
|
|
|
.remove {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: flex-end;
|
|
}
|
|
}
|
|
|
|
.columns {
|
|
& > .col {
|
|
&:not(:first-of-type) {
|
|
height: $input-height;
|
|
}
|
|
|
|
&:first-of-type {
|
|
min-height: $input-height;
|
|
}
|
|
|
|
& > * {
|
|
height: 100%;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|