dashboard/shell/components/GlobalRoleBindings.vue

430 lines
13 KiB
Vue

<script>
import { mapGetters } from 'vuex';
import { MANAGEMENT } from '@shell/config/types';
import { Checkbox } from '@components/Form/Checkbox';
import { _CREATE, _VIEW } from '@shell/config/query-params';
import Loading from '@shell/components/Loading';
import { addObjects, isArray } from '@shell/utils/array';
import { Card } from '@components/Card';
// i18n-uses rbac.globalRoles.types.*.label
// i18n-uses rbac.globalRoles.types.*.description
/**
* Display checkboxes for each global role, checked for given user or principal (group). Can save changes.
*/
export default {
emits: ['hasChanges', 'canLogIn', 'changed'],
components: {
Checkbox,
Loading,
Card
},
props: {
mode: {
type: String,
default: _VIEW,
},
realMode: {
type: String,
default: _VIEW,
},
assignOnly: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'group',
validator(val) {
return val === 'group' || val === 'user';
}
},
groupPrincipalId: {
type: String,
default: ''
},
userId: {
type: String,
default: ''
},
watchOverride: {
type: Boolean,
default: true,
}
},
async fetch() {
try {
this.allRoles = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.GLOBAL_ROLE });
if (!this.sortedRoles) {
this.sortedRoles = {
global: [],
builtin: [],
custom: []
};
this.allRoles.forEach((role) => {
const roleType = this.getRoleType(role);
if (roleType) {
this.sortedRoles[roleType].push(role);
}
});
const sort = (a, b) => a.nameDisplay.localeCompare(b.nameDisplay);
// global roles are not sorted
this.sortedRoles.builtin = this.sortedRoles.builtin.sort(sort);
this.sortedRoles.custom = this.sortedRoles.custom.sort(sort);
if (!this.isCreate) {
this.globalRoleBindings = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.GLOBAL_ROLE_BINDING, opt: { force: true } });
}
// Sort the global roles - use the order defined in 'globalPermissions' and then add the remaining roles after
const globalRoles = [];
const globalRolesAdded = {};
this.globalPermissions.forEach((id) => {
const role = this.sortedRoles.global.find((r) => r.id === id);
if (role) {
globalRoles.push(role);
globalRolesAdded[id] = true;
}
});
// Remaining global roles
const remainingGlobalRoles = this.sortedRoles.global.filter((r) => !globalRolesAdded[r.id]);
this.sortedRoles.global = globalRoles;
this.sortedRoles.global.push(...remainingGlobalRoles);
// End sort of global roles
this.update();
}
} catch (e) { }
},
data() {
return {
// This not only identifies global roles but the order here is the order we want to display them in the UI
globalPermissions: [
'admin',
'user',
'user-base',
],
globalRoleBindings: null,
sortedRoles: null,
selectedRoles: [],
startingSelectedRoles: [],
assignOnlyRoles: {},
roleChanges: {}
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
isCreate() {
return this.realMode === _CREATE;
},
isUser() {
return this.type === 'user';
}
},
watch: {
groupPrincipalId(groupPrincipalId, oldGroupPrincipalId) {
if (groupPrincipalId === oldGroupPrincipalId) {
return;
}
this.update();
},
userId(userId, oldUserId) {
if (userId === oldUserId || this.watchOverride === true) {
return;
}
this.update();
}
},
methods: {
getRoleType(role) {
if (this.globalPermissions.find((p) => p === role.id)) {
return 'global';
} else if (role.builtin) {
return 'builtin';
} else {
return 'custom';
}
},
getUnique(...ids) {
return `${ this.groupPrincipalId || this.userId }-${ ids.join('-') }`;
},
selectDefaults() {
Object.values(this.sortedRoles).forEach((roles) => {
roles.forEach((mappedRole) => {
if (mappedRole.newUserDefault) {
this.selectedRoles.push(mappedRole.id);
}
});
});
},
update() {
this.selectedRoles = [];
this.startingSelectedRoles = [];
this.assignOnlyRoles = {};
if (this.isCreate) {
// Start with the new user default for each role
this.selectDefaults();
} else {
// Start with the principal/user's roles
if (!this.groupPrincipalId && !this.userId) {
return;
}
const boundRoles = this.globalRoleBindings.filter((grb) => {
return this.groupPrincipalId ? grb.groupPrincipalName === this.groupPrincipalId : grb.userName === this.userId;
});
Object.values(this.sortedRoles).forEach((roles) => {
roles.forEach((mappedRole) => {
const boundRole = boundRoles.find((boundRole) => boundRole.globalRoleName === mappedRole.id);
if (!!boundRole) {
this.selectedRoles.push(mappedRole.id);
this.startingSelectedRoles.push({
roleId: mappedRole.id,
bindingId: boundRole.id
});
// Checkboxes should be disabled, besides normal 'mode' ways, if we're only assigning and not removing existing roles
this.assignOnlyRoles[mappedRole.id] = this.assignOnly;
}
});
});
if (this.assignOnly && !this.selectedRoles.length) {
// If we're assigning roles to a group that has no existing roles start with the default roles selected
this.selectDefaults();
}
}
// Force an update to pump out the initial state
this.checkboxChanged();
},
checkboxChanged() {
const addRoles = this.selectedRoles
.filter((selected) => !this.startingSelectedRoles.find((startingRole) => startingRole.roleId === selected));
const removeBindings = this.startingSelectedRoles
.filter((startingRole) => !this.selectedRoles.find((selected) => selected === startingRole.roleId))
.map((startingRole) => startingRole.bindingId);
this.roleChanges = {
initialRoles: this.startingSelectedRoles,
addRoles,
removeBindings
};
this.$emit('hasChanges', !!this.roleChanges.addRoles.length || !!this.roleChanges.removeBindings.length);
this.$emit('canLogIn', this.confirmUserCanLogIn());
this.$emit('changed', this.roleChanges);
},
async saveAddedRoles(userId) {
const requestOptions = {
type: MANAGEMENT.GLOBAL_ROLE_BINDING,
metadata: { generateName: `grb-` },
};
if (this.groupPrincipalId) {
requestOptions.groupPrincipalName = this.groupPrincipalId;
} else {
requestOptions.userName = userId || this.userId;
}
const newBindings = await Promise.all(this.roleChanges.addRoles.map((role) => this.$store.dispatch(`management/create`, {
...requestOptions,
globalRoleName: role,
})));
// Save all changes (and ensure user isn't logged out if they don't have permissions to make a change)
await Promise.all(newBindings.map((newBinding) => newBinding.save({ redirectUnauthorized: false })));
},
async saveRemovedRoles() {
const existingBindings = await Promise.all(this.roleChanges.removeBindings.map((bindingId) => this.$store.dispatch('management/find', {
type: MANAGEMENT.GLOBAL_ROLE_BINDING,
id: bindingId
})));
// Save all changes (and ensure user isn't logged out if they don't have permissions to make a change)
await Promise.all(existingBindings.map((existingBinding) => existingBinding.remove({ redirectUnauthorized: false })));
},
/**
* userId is optional, used when a user has just been created
*/
async save(userId) {
// Ensure roles are added before removed (in case by removing one user is unable to add another)
await this.saveAddedRoles(userId);
await this.saveRemovedRoles();
await this.$store.dispatch('management/findAll', {
type: MANAGEMENT.GLOBAL_ROLE_BINDING,
opt: { force: true }
}, { force: true });
},
confirmUserCanLogIn() {
const allRolesRules = [];
Object.values(this.sortedRoles).forEach((roles) => {
roles.forEach((mappedRole) => {
if (this.selectedRoles.includes(mappedRole.id)) {
addObjects(allRolesRules, mappedRole.rules || []);
}
});
});
return allRolesRules.some((rule) => this.isRuleValid(rule));
},
isRuleValid(rule) {
// Brought over from Ember
if (( rule.resources || [] ).some(resourceValidator) && ( rule.apiGroups || [] ).some(apiGroupValidator) && verbsValidator(( rule.verbs || [] ))) {
return true;
}
return false;
function resourceValidator(resource) {
const resourcesRequiredForLogin = ['*', 'preferences', 'settings', 'features'];
// console.log(`resourceValidator status: `, resourcesRequiredForLogin.includes(resource), resource);
return resourcesRequiredForLogin.includes(resource);
}
function apiGroupValidator(apiGroup) {
const apiGroupsRequiredForLogin = ['*', 'management.cattle.io'];
// console.log(`apiGroupsRequiredForLogin status: `, apiGroupsRequiredForLogin.includes(apiGroup), apiGroup);
return apiGroupsRequiredForLogin.includes(apiGroup);
}
function verbsValidator(verbs) {
const restrictedTarget = ['get', 'list', 'watch'];
const verbsRequiredForLogin = ['*', ...restrictedTarget];
if (isArray(verbs) && verbs.length > 1) {
// console.log(`verbsRequiredForLogin status 1: `, restrictedTarget.every(rt => verbs.includes(rt)), verbs);
return restrictedTarget.every((rt) => verbs.includes(rt));
}
// console.log(`verbsRequiredForLogin status 2: `, verbsRequiredForLogin.includes(verbs[0]), verbsRequiredForLogin, verbs);
return verbsRequiredForLogin.includes(verbs[0]);
}
},
}
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else>
<form v-if="selectedRoles">
<div
v-for="(sortedRole, roleType) in sortedRoles"
:key="roleType"
class="role-group mb-10"
>
<Card
v-if="Object.keys(sortedRole).length"
:show-highlight-border="false"
:show-actions="false"
>
<template v-slot:title>
<div class="type-title">
<h3>{{ t(`rbac.globalRoles.types.${roleType}.label`) }}</h3>
<div class="type-description">
{{ t(`rbac.globalRoles.types.${roleType}.description`, { isUser }) }}
</div>
</div>
</template>
<template v-slot:body>
<div
class="checkbox-section"
:class="'checkbox-section--' + roleType"
>
<div
v-for="(role, i) in sortedRoles[roleType]"
:key="i"
class="checkbox mb-10 mr-10"
>
<Checkbox
v-model:value="selectedRoles"
:value-when-true="role.id"
:disabled="!!assignOnlyRoles[role.id]"
:label="role.nameDisplay"
:description="role.descriptionDisplay"
:mode="mode"
:data-testId="'grb-checkbox-' + role.id"
@update:value="checkboxChanged"
>
<template #label>
<div class="checkbox-label-slot">
<span class="checkbox-label">{{ role.nameDisplay }}</span>
<i
v-if="!!assignOnlyRoles[role.id]"
v-clean-tooltip="t('rbac.globalRoles.assignOnlyRole')"
class="checkbox-info icon icon-info icon-lg"
/>
</div>
</template>
</Checkbox>
</div>
</div>
</template>
</Card>
</div>
</form>
</div>
</template>
<style lang='scss'>
.role-group {
.card-container {
margin: 0;
}
}
</style>
<style lang='scss' scoped>
$detailSize: 11px;
.deprecation-notice {
margin: 8px 0 8px 20px;
}
.role-group {
.type-title {
display: flex;
flex-direction: column;
.type-description {
font-size: $detailSize;
}
}
.checkbox-section {
display: grid;
grid-template-columns: repeat(3, 1fr);
&--global {
grid-template-columns: 100%;
}
.checkbox-label {
&-slot {
display: inline-flex;
align-items: center;
}
color: var(--body-text);
margin: 0;
}
}
}
</style>