Skip to content

Commit

Permalink
feat: add multi-role assignment support for users (#7037)
Browse files Browse the repository at this point in the history
#### What type of PR is this?

/area ui
/kind feature
/milestone 2.20.x

#### What this PR does / why we need it:

支持为用户分配多个角色。

<img width="634" alt="image" src="https://github.com/user-attachments/assets/caa40327-518a-4bef-afc3-75a020018d3d">
<img width="764" alt="image" src="https://github.com/user-attachments/assets/8b4b807e-6c72-45d9-9368-75e70bb12dcc">

TODO:

- [x] Console / UC 侧边栏显示多个角色
- [x] 支持在管理端查看用户聚合的角色模板列表,或者在分配时显示所选角色其下的角色模板列表,能够让管理员清楚的知道用户具体权限。

#### Which issue(s) this PR fixes:

Fixes #

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?

```release-note
支持为用户分配多个角色。
```
  • Loading branch information
ruibaby authored Nov 24, 2024
1 parent 964bc28 commit 391aac6
Show file tree
Hide file tree
Showing 14 changed files with 403 additions and 90 deletions.
46 changes: 42 additions & 4 deletions ui/console-src/layouts/BasicLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import { coreMenuGroups } from "@console/router/constant";
import {
Dialog,
IconAccountCircleLine,
IconArrowDownLine,
IconLogoutCircleRLine,
IconMore,
IconSearch,
IconUserSettings,
IconShieldUser,
VAvatar,
VDropdown,
VTag,
} from "@halo-dev/components";
import { useEventListener } from "@vueuse/core";
Expand Down Expand Up @@ -152,17 +154,52 @@ onMounted(() => {
>
{{ currentUser?.spec.displayName }}
</div>
<div v-if="currentRoles?.[0]" class="flex">
<VTag>
<div v-if="currentRoles?.length" class="flex mt-1">
<VTag v-if="currentRoles.length === 1">
<template #leftIcon>
<IconUserSettings />
<IconShieldUser />
</template>
{{
currentRoles[0].metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
] || currentRoles[0].metadata.name
}}
</VTag>
<VDropdown v-else>
<div class="flex gap-1">
<VTag>
<template #leftIcon>
<IconShieldUser />
</template>
{{ $t("core.sidebar.profile.aggregate_role") }}
</VTag>
<IconArrowDownLine />
</div>
<template #popper>
<div class="p-1">
<h2
class="text-gray-600 text-sm font-semibold border-b border-gray-100 pb-1.5"
>
{{ $t("core.sidebar.profile.aggregate_role") }}
</h2>
<div class="flex gap-2 flex-wrap mt-2">
<VTag
v-for="role in currentRoles"
:key="role.metadata.name"
>
<template #leftIcon>
<IconShieldUser />
</template>
{{
role.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
] || role.metadata.name
}}
</VTag>
</div>
</div>
</template>
</VDropdown>
</div>
</div>

Expand Down Expand Up @@ -299,6 +336,7 @@ onMounted(() => {
.profile-placeholder {
height: 70px;
flex: none;
.current-profile {
height: 70px;
Expand Down
6 changes: 3 additions & 3 deletions ui/console-src/modules/system/roles/RoleDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,8 @@ const handleUpdateRole = async () => {
<div>
<dl class="divide-y divide-gray-100">
<div
v-for="(group, groupIndex) in roleTemplateGroups"
:key="groupIndex"
v-for="(group, index) in roleTemplateGroups"
:key="index"
class="bg-white px-4 py-5 hover:bg-gray-50 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
>
<dt class="text-sm font-medium text-gray-900">
Expand Down Expand Up @@ -224,7 +224,7 @@ const handleUpdateRole = async () => {
</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<ul class="space-y-2">
<li v-for="(role, index) in group.roles" :key="index">
<li v-for="role in group.roles" :key="role.metadata.name">
<label
class="inline-flex w-72 cursor-pointer flex-row items-center gap-4 rounded-base border p-5 hover:border-primary"
>
Expand Down
14 changes: 10 additions & 4 deletions ui/console-src/modules/system/users/UserDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const tabbarItems = computed(() => {
}));
});
const handleDelete = async (userToDelete: User) => {
const handleDelete = async (user: User) => {
Dialog.warning({
title: t("core.user.operations.delete.title"),
description: t("core.common.dialog.descriptions.cannot_be_recovered"),
Expand All @@ -111,7 +111,7 @@ const handleDelete = async (userToDelete: User) => {
onConfirm: async () => {
try {
await coreApiClient.user.deleteUser({
name: userToDelete.metadata.name,
name: user.metadata.name,
});
Toast.success(t("core.common.toast.delete_success"));
router.push({ name: "Users" });
Expand Down Expand Up @@ -189,11 +189,17 @@ function onGrantPermissionModalClose() {
<VDropdownItem @click="passwordChangeModal = true">
{{ $t("core.user.detail.actions.change_password.title") }}
</VDropdownItem>
<VDropdownItem @click="grantPermissionModal = true">
<VDropdownItem
v-if="currentUser?.metadata.name !== user?.user.metadata.name"
@click="grantPermissionModal = true"
>
{{ $t("core.user.detail.actions.grant_permission.title") }}
</VDropdownItem>
<VDropdownItem
v-if="user?.user"
v-if="
user &&
currentUser?.metadata.name !== user?.user.metadata.name
"
type="danger"
@click="handleDelete(user.user)"
>
Expand Down
20 changes: 11 additions & 9 deletions ui/console-src/modules/system/users/UserList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
IconAddCircle,
IconLockPasswordLine,
IconRefreshLine,
IconUserFollow,
IconShieldUser,
IconUserSettings,
Toast,
VAvatar,
Expand Down Expand Up @@ -288,7 +288,7 @@ function onGrantPermissionModalClose() {
type="default"
>
<template #icon>
<IconUserFollow class="h-full w-full" />
<IconShieldUser class="h-full w-full" />
</template>
{{ $t("core.user.actions.roles") }}
</VButton>
Expand Down Expand Up @@ -469,19 +469,21 @@ function onGrantPermissionModalClose() {
<template #end>
<VEntityField>
<template #description>
<div
v-for="(role, roleIndex) in user.roles"
:key="roleIndex"
class="flex items-center"
>
<VTag>
<VSpace>
<VTag
v-for="role in user.roles"
:key="role.metadata.name"
>
<template #leftIcon>
<IconShieldUser />
</template>
{{
role.metadata.annotations?.[
rbacAnnotations.DISPLAY_NAME
] || role.metadata.name
}}
</VTag>
</div>
</VSpace>
</template>
</VEntityField>
<VEntityField v-if="user.user.metadata.deletionTimestamp">
Expand Down
152 changes: 123 additions & 29 deletions ui/console-src/modules/system/users/components/GrantPermissionModal.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
<script lang="ts" setup>
import SubmitButton from "@/components/button/SubmitButton.vue";
import { rbacAnnotations } from "@/constants/annotations";
import { SUPER_ROLE_NAME } from "@/constants/constants";
import { roleLabels } from "@/constants/labels";
import type { User } from "@halo-dev/api-client";
import { consoleApiClient } from "@halo-dev/api-client";
import { VButton, VModal, VSpace } from "@halo-dev/components";
import { ref } from "vue";
import { consoleApiClient, coreApiClient } from "@halo-dev/api-client";
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
import { useMutation, useQuery } from "@tanstack/vue-query";
import { computed, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import RolesView from "./RolesView.vue";
const { t } = useI18n();
const props = withDefaults(
defineProps<{
Expand All @@ -19,51 +27,137 @@ const emit = defineEmits<{
}>();
const modal = ref<InstanceType<typeof VModal> | null>(null);
const selectedRole = ref("");
const isSubmitting = ref(false);
const handleGrantPermission = async () => {
try {
isSubmitting.value = true;
await consoleApiClient.user.grantPermission({
const selectedRoleNames = ref<string[]>([]);
onMounted(() => {
if (!props.user) {
return;
}
selectedRoleNames.value = JSON.parse(
props.user.metadata.annotations?.[rbacAnnotations.ROLE_NAMES] || "[]"
);
});
const { mutate, isLoading } = useMutation({
mutationKey: ["core:user:grant-permissions"],
mutationFn: async ({ roles }: { roles: string[] }) => {
return await consoleApiClient.user.grantPermission({
name: props.user?.metadata.name as string,
grantRequest: {
roles: [selectedRole.value],
roles: roles,
},
});
},
onSuccess() {
Toast.success(t("core.common.toast.operation_success"));
modal.value?.close();
} catch (error) {
console.error("Failed to grant permission to user", error);
} finally {
isSubmitting.value = false;
},
});
function onSubmit(data: { roles: string[] }) {
mutate({ roles: data.roles });
}
const { data: allRoles } = useQuery({
queryKey: ["core:roles"],
queryFn: async () => {
const { data } = await coreApiClient.role.listRole({
page: 0,
size: 0,
labelSelector: [`!${roleLabels.TEMPLATE}`],
});
return data;
},
});
const { data: allRoleTemplates } = useQuery({
queryKey: ["core:role-templates"],
queryFn: async () => {
const { data } = await coreApiClient.role.listRole({
page: 0,
size: 0,
labelSelector: [`${roleLabels.TEMPLATE}=true`, "!halo.run/hidden"],
});
return data.items;
},
});
const currentRoleTemplates = computed(() => {
if (!selectedRoleNames.value.length) {
return [];
}
};
const selectedRoles = allRoles.value?.items.filter((role) =>
selectedRoleNames.value.includes(role.metadata.name)
);
let allDependsRoleTemplates: string[] = [];
selectedRoles?.forEach((role) => {
allDependsRoleTemplates = allDependsRoleTemplates.concat(
JSON.parse(
role.metadata.annotations?.[rbacAnnotations.DEPENDENCIES] || "[]"
)
);
});
return allRoleTemplates.value?.filter((item) => {
return allDependsRoleTemplates.includes(item.metadata.name);
});
});
</script>

<template>
<VModal
ref="modal"
:title="$t('core.user.grant_permission_modal.title')"
:width="500"
:width="600"
:centered="false"
@close="emit('close')"
>
<FormKit
id="grant-permission-form"
name="grant-permission-form"
:config="{ validationVisibility: 'submit' }"
type="form"
@submit="handleGrantPermission"
>
<div>
<FormKit
v-model="selectedRole"
:label="$t('core.user.grant_permission_modal.fields.role.label')"
type="roleSelect"
></FormKit>
</FormKit>
id="grant-permission-form"
name="grant-permission-form"
:config="{ validationVisibility: 'submit' }"
type="form"
@submit="onSubmit"
>
<!-- @vue-ignore -->
<FormKit
v-model="selectedRoleNames"
multiple
name="roles"
:label="$t('core.user.grant_permission_modal.fields.role.label')"
type="roleSelect"
:placeholder="
$t('core.user.grant_permission_modal.fields.role.placeholder')
"
></FormKit>
</FormKit>

<div v-if="selectedRoleNames.length">
<div
v-if="selectedRoleNames.includes(SUPER_ROLE_NAME)"
class="text-sm text-gray-600 mt-4"
>
{{ $t("core.user.grant_permission_modal.roles_preview.all") }}
</div>

<div v-else-if="currentRoleTemplates?.length" class="space-y-3 mt-4">
<span class="text-sm text-gray-600">
{{ $t("core.user.grant_permission_modal.roles_preview.includes") }}
</span>
<RolesView :role-templates="currentRoleTemplates" />
</div>
</div>
</div>

<template #footer>
<VSpace>
<SubmitButton
:loading="isSubmitting"
:loading="isLoading"
type="secondary"
:text="$t('core.common.buttons.submit')"
@submit="$formkit.submit('grant-permission-form')"
Expand Down
Loading

0 comments on commit 391aac6

Please sign in to comment.