Browse Source

feat: support view privilege groups (#750)

* add privilege groups APIs

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* add more api

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* more types

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* reorg

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* init group client

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* update group UI

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* support create/edit privilegeGroup

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* support drop privilege group

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* update privilege group UI

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* UI update

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* update RBAC dialog

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* disable create privilege group

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* show pagination for role page

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

---------

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
ryjiang 5 months ago
parent
commit
6ee112784a

+ 37 - 1
client/src/http/User.service.ts

@@ -1,5 +1,10 @@
 import BaseModel from './BaseModel';
-import type { Users, UsersWithRoles } from '@server/types';
+import type {
+  Users,
+  UsersWithRoles,
+  PrivilegeGroup,
+  PrivilegeGroupsRes,
+} from '@server/types';
 import type {
   CreateUserParams,
   DeleteUserParams,
@@ -8,6 +13,7 @@ import type {
   DeleteRoleParams,
   AssignRoleParams,
   UnassignRoleParams,
+  CreatePrivilegeGroupParams,
 } from '../pages/user/Types';
 
 export class UserService extends BaseModel {
@@ -70,6 +76,14 @@ export class UserService extends BaseModel {
     });
   }
 
+  // get RBAC info
+  static getAllPrivilegeGroups() {
+    return super.search({
+      path: `/users/privilegeGroups`,
+      params: {},
+    }) as Promise<PrivilegeGroup[]>;
+  }
+
   // get RBAC info
   static getRBAC() {
     return super.search({
@@ -83,4 +97,26 @@ export class UserService extends BaseModel {
       Privileges: Record<string, unknown>;
     }>;
   }
+  // get privilege groups
+  static getPrivilegeGroups() {
+    return super.search<PrivilegeGroupsRes>({
+      path: `/users/privilege-groups`,
+      params: {},
+    });
+  }
+  // create privilege group
+  static createPrivilegeGroup(data: CreatePrivilegeGroupParams) {
+    return super.create({ path: `/users/privilege-groups`, data });
+  }
+  // update privilege group
+  static updatePrivilegeGroup(data: CreatePrivilegeGroupParams) {
+    return super.update({
+      path: `/users/privilege-groups/${data.group_name}`,
+      data,
+    });
+  }
+  // delete privilege group
+  static deletePrivilegeGroup(data: { group_name: string }) {
+    return super.delete({ path: `/users/privilege-groups/${data.group_name}` });
+  }
 }

+ 11 - 0
client/src/i18n/cn/user.ts

@@ -1,3 +1,5 @@
+import PrivilegeGroups from '@/pages/user/PrivilegeGroups';
+
 const userTrans = {
   createTitle: '创建用户',
   updateTitle: '更新Milvus用户',
@@ -31,6 +33,15 @@ const userTrans = {
   objectUser: '用户',
 
   forceDelLabel: '强制删除,撤销所有权限。',
+
+  // Privilege Groups
+  privilegeGroups: '权限组',
+  privilegeGroup: '权限组',
+  name: '名称',
+  editPrivilegeGroup: '编辑权限组',
+  deletePrivilegGroupWarning: '您正在尝试删除权限组,请确保没有角色与其绑定。',
+  createPrivilegeGroupTitle: '创建权限组',
+  updatePrivilegeGroupTitle: '更新权限组',
 };
 
 export default userTrans;

+ 12 - 0
client/src/i18n/en/user.ts

@@ -1,3 +1,5 @@
+import { create } from "domain";
+
 const userTrans = {
   createTitle: 'Create User',
   updateTitle: 'Update Milvus User',
@@ -32,6 +34,16 @@ const userTrans = {
   objectUser: 'User',
 
   forceDelLabel: 'Force delete, revoke all privileges.',
+
+  // Privilege Groups
+  privilegeGroups: 'Privilege Groups',
+  privilegeGroup: 'Privilege Group',
+  name: 'Name',
+  editPrivilegeGroup: 'Edit Privilege Group',
+  deletePrivilegGroupWarning:
+    'You are trying to drop the privilege group, please make sure no role is bound to it.',
+  createPrivilegeGroupTitle: 'Create Privilege Group',
+  updatePrivilegeGroupTitle: 'Update Privilege Group',
 };
 
 export default userTrans;

+ 2 - 1
client/src/pages/index.tsx

@@ -64,7 +64,8 @@ function Index() {
 
     if (
       location.pathname.includes('users') ||
-      location.pathname.includes('roles')
+      location.pathname.includes('roles') ||
+      location.pathname.includes('privilege-groups')
     ) {
       return navTrans('user');
     }

+ 227 - 0
client/src/pages/user/PrivilegeGroups.tsx

@@ -0,0 +1,227 @@
+import { useContext, useEffect, useState } from 'react';
+import { Theme, Chip } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+import { UserService } from '@/http';
+import { rootContext } from '@/context';
+import { useNavigationHook, usePaginationHook } from '@/hooks';
+import AttuGrid from '@/components/grid/Grid';
+import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
+import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
+import { ALL_ROUTER_TYPES } from '@/router/consts';
+import UpdatePrivilegeGroupDialog from './dialogs/UpdatePrivilegeGroupDialog';
+import { makeStyles } from '@mui/styles';
+import { PrivilegeGroup } from '@server/types';
+import { getLabelDisplayedRows } from '@/pages/search/Utils';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    height: `calc(100vh - 160px)`,
+  },
+  chip: {
+    marginRight: theme.spacing(0.5),
+  },
+}));
+
+const PrivilegeGroups = () => {
+  useNavigationHook(ALL_ROUTER_TYPES.USER);
+  const classes = useStyles();
+
+  const [groups, setGroups] = useState<PrivilegeGroup[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [selectedGroups, setSelectedGroups] = useState<PrivilegeGroup[]>([]);
+  const { setDialog, handleCloseDialog, openSnackBar } =
+    useContext(rootContext);
+  const { t: successTrans } = useTranslation('success');
+  const { t: userTrans } = useTranslation('user');
+  const { t: btnTrans } = useTranslation('btn');
+  const { t: dialogTrans } = useTranslation('dialog');
+
+  const fetchGroups = async () => {
+    setLoading(true);
+    const res = await UserService.getPrivilegeGroups();
+    setGroups(res.privilege_groups);
+
+    setLoading(false);
+  };
+
+  const onUpdate = async (data: { isEditing: boolean }) => {
+    handleCloseDialog();
+    await fetchGroups();
+    // update selected groups
+    setSelectedGroups([]);
+    // open snackbar
+    openSnackBar(
+      successTrans(data.isEditing ? 'update' : 'create', {
+        name: userTrans('privilegeGroup'),
+      })
+    );
+  };
+
+  const handleDelete = async () => {
+    // delete selected groups
+    for (const group of selectedGroups) {
+      await UserService.deletePrivilegeGroup({ group_name: group.group_name });
+    }
+    // open snackbar
+    openSnackBar(successTrans('delete', { name: userTrans('privilegeGroup') }));
+    // update groups
+    await fetchGroups();
+    // update selected groups
+    setSelectedGroups([]);
+    // close dialog
+    handleCloseDialog();
+  };
+
+  useEffect(() => {
+    fetchGroups();
+  }, []);
+
+  const {
+    pageSize,
+    handlePageSize,
+    currentPage,
+    handleCurrentPage,
+    total,
+    data: result,
+    order,
+    orderBy,
+    handleGridSort,
+  } = usePaginationHook(groups || []);
+
+  const toolbarConfigs: ToolBarConfig[] = [
+    {
+      label: userTrans('privilegeGroup'),
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <UpdatePrivilegeGroupDialog
+                onUpdate={onUpdate}
+                handleClose={handleCloseDialog}
+              />
+            ),
+          },
+        });
+      },
+      icon: 'add',
+    },
+    {
+      type: 'button',
+      btnVariant: 'text',
+      btnColor: 'secondary',
+      label: userTrans('editPrivilegeGroup'),
+      onClick: async () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <UpdatePrivilegeGroupDialog
+                group={selectedGroups[0]}
+                onUpdate={onUpdate}
+                handleClose={handleCloseDialog}
+              />
+            ),
+          },
+        });
+      },
+      disabled: () => selectedGroups.length !== 1,
+      icon: 'edit',
+    },
+    {
+      type: 'button',
+      btnVariant: 'text',
+      btnColor: 'secondary',
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <DeleteTemplate
+                label={btnTrans('drop')}
+                title={dialogTrans('deleteTitle', {
+                  type: userTrans('privilegeGroup'),
+                })}
+                text={userTrans('deletePrivilegGroupWarning')}
+                handleDelete={handleDelete}
+              />
+            ),
+          },
+        });
+      },
+      label: btnTrans('drop'),
+      disabledTooltip: userTrans('deleteTip'),
+      icon: 'delete',
+    },
+  ];
+
+  const handlePageChange = (e: any, page: number) => {
+    handleCurrentPage(page);
+  };
+
+  const colDefinitions: ColDefinitionsType[] = [
+    {
+      id: 'group_name',
+      align: 'left',
+      disablePadding: false,
+      label: userTrans('name'),
+    },
+
+    {
+      id: 'privileges',
+      align: 'left',
+      disablePadding: false,
+      formatter({ privileges }) {
+        return (
+          <div>
+            {privileges.map((privilege: { name: string }, index: number) => (
+              <Chip
+                key={index}
+                label={privilege.name}
+                className={classes.chip}
+                color="primary"
+                variant="filled"
+              />
+            ))}
+          </div>
+        );
+      },
+      label: userTrans('privileges'),
+    },
+  ];
+
+  const handleSelectChange = (groups: PrivilegeGroup[]) => {
+    setSelectedGroups(groups);
+  };
+
+  return (
+    <div className={classes.wrapper}>
+      <AttuGrid
+        toolbarConfigs={[]}
+        colDefinitions={colDefinitions}
+        rows={result}
+        rowCount={total}
+        primaryKey="group_name"
+        showPagination={true}
+        selected={selectedGroups}
+        setSelected={handleSelectChange}
+        page={currentPage}
+        onPageChange={handlePageChange}
+        rowsPerPage={pageSize}
+        setRowsPerPage={handlePageSize}
+        isLoading={loading}
+        order={order}
+        orderBy={orderBy}
+        rowHeight={49}
+        openCheckBox={false}
+        handleSort={handleGridSort}
+        labelDisplayedRows={getLabelDisplayedRows(userTrans('privilegeGroups'))}
+      />
+    </div>
+  );
+};
+
+export default PrivilegeGroups;

+ 36 - 14
client/src/pages/user/Roles.tsx

@@ -3,14 +3,18 @@ import { Theme, Chip } from '@mui/material';
 import { useTranslation } from 'react-i18next';
 import { UserService } from '@/http';
 import { rootContext, dataContext } from '@/context';
-import { useNavigationHook } from '@/hooks';
+import { useNavigationHook, usePaginationHook } from '@/hooks';
 import AttuGrid from '@/components/grid/Grid';
 import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
+import UpdateRoleDialog from './dialogs/UpdateRoleDialog';
 import { ALL_ROUTER_TYPES } from '@/router/consts';
-import UpdateRoleDialog from './UpdateRoleDialog';
 import { makeStyles } from '@mui/styles';
-import type { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
+import type {
+  ColDefinitionsType,
+  ToolBarConfig,
+} from '@/components/grid/Types';
 import type { DeleteRoleParams, RoleData } from './Types';
+import { getLabelDisplayedRows } from '@/pages/search/Utils';
 
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
@@ -25,6 +29,7 @@ const Roles = () => {
   useNavigationHook(ALL_ROUTER_TYPES.USER);
   const classes = useStyles();
   const { database } = useContext(dataContext);
+  const [loading, setLoading] = useState(false);
 
   const [roles, setRoles] = useState<RoleData[]>([]);
   const [selectedRole, setSelectedRole] = useState<RoleData[]>([]);
@@ -198,25 +203,42 @@ const Roles = () => {
     fetchRoles();
   }, [database]);
 
+  const {
+    pageSize,
+    handlePageSize,
+    currentPage,
+    handleCurrentPage,
+    total,
+    data: result,
+    order,
+    orderBy,
+    handleGridSort,
+  } = usePaginationHook(roles || []);
+
+  const handlePageChange = (e: any, page: number) => {
+    handleCurrentPage(page);
+  };
+
   return (
     <div className={classes.wrapper}>
       <AttuGrid
         toolbarConfigs={toolbarConfigs}
         colDefinitions={colDefinitions}
-        rows={roles}
-        rowCount={roles.length}
+        rows={result}
+        rowCount={total}
         primaryKey="name"
-        showPagination={false}
+        showPagination={true}
         selected={selectedRole}
         setSelected={handleSelectChange}
-        // page={currentPage}
-        // onPageChange={handlePageChange}
-        // rowsPerPage={pageSize}
-        // setRowsPerPage={handlePageSize}
-        // isLoading={loading}
-        // order={order}
-        // orderBy={orderBy}
-        // handleSort={handleGridSort}
+        page={currentPage}
+        onPageChange={handlePageChange}
+        rowsPerPage={pageSize}
+        setRowsPerPage={handlePageSize}
+        isLoading={loading}
+        order={order}
+        orderBy={orderBy}
+        handleSort={handleGridSort}
+        labelDisplayedRows={getLabelDisplayedRows(userTrans('roles'))}
       />
     </div>
   );

+ 12 - 0
client/src/pages/user/Types.ts

@@ -58,6 +58,11 @@ export interface CreateRoleParams {
   privileges: Privilege[];
 }
 
+export interface CreatePrivilegeGroupParams {
+  group_name: string;
+  privileges: string[];
+}
+
 export interface RoleData {
   name: string;
   privileges: Privilege[];
@@ -93,6 +98,13 @@ export interface PrivilegeOptionsProps {
   title: string;
 }
 
+export interface PrivilegeGrpOptionsProps {
+  options: string[];
+  selection: string[];
+  onChange: (selection: string[]) => void;
+  group_name: string;
+}
+
 export type RBACOptions = {
   GlobalPrivileges: Record<string, unknown>;
   CollectionPrivileges: Record<string, unknown>;

+ 3 - 3
client/src/pages/user/User.tsx

@@ -14,10 +14,10 @@ import {
 import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
 import { rootContext } from '@/context';
 import { useNavigationHook, usePaginationHook } from '@/hooks';
+import CreateUser from './dialogs/CreateUserDialog';
+import UpdateUserRole from './dialogs/UpdateUserRole';
+import UpdateUser from './dialogs/UpdateUserPassDialog';
 import { ALL_ROUTER_TYPES } from '@/router/consts';
-import CreateUser from './CreateUser';
-import UpdateUserRole from './UpdateUserRole';
-import UpdateUser from './Update';
 import { makeStyles } from '@mui/styles';
 
 const useStyles = makeStyles((theme: Theme) => ({

+ 6 - 0
client/src/pages/user/Users.tsx → client/src/pages/user/UsersAndRoles.tsx

@@ -6,6 +6,7 @@ import { ALL_ROUTER_TYPES } from '@/router/consts';
 import RouteTabList from '@/components/customTabList/RouteTabList';
 import User from './User';
 import Roles from './Roles';
+import PrivilegeGroups from './PrivilegeGroups';
 import { makeStyles } from '@mui/styles';
 import type { ITab } from '@/components/customTabList/Types';
 
@@ -52,6 +53,11 @@ const Users = () => {
       component: <Roles />,
       path: 'roles',
     },
+    {
+      label: userTrans('privilegeGroups'),
+      component: <PrivilegeGroups />,
+      path: 'privilege-groups',
+    },
   ];
 
   const activeTabIndex = tabs.findIndex(t => t.path === currentPath);

+ 1 - 1
client/src/pages/user/CreateUser.tsx → client/src/pages/user/dialogs/CreateUserDialog.tsx

@@ -12,7 +12,7 @@ import CustomInput from '@/components/customInput/CustomInput';
 import { useFormValidation } from '@/hooks';
 import { formatForm } from '@/utils';
 import { makeStyles } from '@mui/styles';
-import type { CreateUserProps, CreateUserParams } from './Types';
+import type { CreateUserProps, CreateUserParams } from '../Types';
 import type { Option as RoleOption } from '@/components/customSelector/Types';
 import type { ITextfieldConfig } from '@/components/customInput/Types';
 

+ 66 - 0
client/src/pages/user/dialogs/PrivilegeGroupOptions.tsx

@@ -0,0 +1,66 @@
+import {
+  Theme,
+  Typography,
+  Checkbox,
+  FormGroup,
+  FormControlLabel,
+} from '@mui/material';
+import { FC } from 'react';
+import { PrivilegeGrpOptionsProps } from '../Types';
+import { makeStyles } from '@mui/styles';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  checkBox: {
+    width: theme.spacing(24),
+  },
+  formGrp: {
+    marginBottom: theme.spacing(2),
+  },
+  subTitle: {
+    marginBottom: theme.spacing(0.5),
+  },
+}));
+
+const PrivilegeGroupOptions: FC<PrivilegeGrpOptionsProps> = ({
+  options,
+  selection,
+  onChange,
+}) => {
+  const classes = useStyles();
+
+  return (
+    <>
+      <FormGroup row className={classes.formGrp}>
+        {options.map((r: string) => (
+          <FormControlLabel
+            control={
+              <Checkbox
+                onChange={(
+                  e: React.ChangeEvent<HTMLInputElement>,
+                  checked: boolean
+                ) => {
+                  let newSelection = [...selection];
+
+                  if (!checked) {
+                    newSelection = newSelection.filter((n: string) => n !== r);
+                  } else {
+                    newSelection.push(r);
+                  }
+
+                  onChange(newSelection);
+                }}
+              />
+            }
+            key={r}
+            label={r}
+            value={r}
+            checked={selection.filter(s => s === r).length > 0 ? true : false}
+            className={classes.checkBox}
+          />
+        ))}
+      </FormGroup>
+    </>
+  );
+};
+
+export default PrivilegeGroupOptions;

+ 1 - 1
client/src/pages/user/PrivilegeOptions.tsx → client/src/pages/user/dialogs/PrivilegeOptions.tsx

@@ -7,7 +7,7 @@ import {
 } from '@mui/material';
 import { FC } from 'react';
 import { makeStyles } from '@mui/styles';
-import type { Privilege, PrivilegeOptionsProps } from './Types';
+import type { Privilege, PrivilegeOptionsProps } from '../Types';
 
 const useStyles = makeStyles((theme: Theme) => ({
   checkBox: {

+ 275 - 0
client/src/pages/user/dialogs/UpdatePrivilegeGroupDialog.tsx

@@ -0,0 +1,275 @@
+import {
+  Theme,
+  Typography,
+  Accordion,
+  AccordionSummary,
+  AccordionDetails,
+  Checkbox,
+} from '@mui/material';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import { FC, useMemo, useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import DialogTemplate from '@/components/customDialog/DialogTemplate';
+import CustomInput from '@/components/customInput/CustomInput';
+import { ITextfieldConfig } from '@/components/customInput/Types';
+import { useFormValidation } from '@/hooks';
+import { formatForm } from '@/utils';
+import { UserService } from '@/http';
+import { CreatePrivilegeGroupParams } from '../Types';
+import PrivilegeGroupOptions from './PrivilegeGroupOptions';
+import { makeStyles } from '@mui/styles';
+import { PrivilegeGroup } from '@server/types';
+import { Opacity } from '@mui/icons-material';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  input: {
+    margin: theme.spacing(1, 0, 0.5),
+  },
+  dialogWrapper: {
+    maxWidth: theme.spacing(88),
+  },
+  checkBox: {
+    width: theme.spacing(24),
+  },
+  formGrp: {
+    marginBottom: theme.spacing(2),
+  },
+  subTitle: {
+    marginBottom: theme.spacing(0.5),
+  },
+
+  accordin: {
+    margin: 0,
+    '&.Mui-expanded': {
+      opacity: 1,
+    },
+    border: `1px solid ${theme.palette.divider}`,
+    borderBottom: 'none',
+  },
+  accordionSummary: {
+    backgroundColor: theme.palette.background.default,
+    minHeight: '48px !important',
+    '& .MuiAccordionSummary-content': {
+      margin: 0,
+      alignItems: 'center',
+      position: 'relative',
+      left: -10,
+    },
+  },
+  accordionDetail: {
+    backgroundColor: theme.palette.background.light,
+    borderTop: 'none',
+  },
+}));
+
+export interface CreatePrivilegeGroupProps {
+  onUpdate: (data: {
+    data: CreatePrivilegeGroupParams;
+    isEditing: boolean;
+  }) => void;
+  handleClose: () => void;
+  group?: PrivilegeGroup;
+}
+
+const UpdateRoleDialog: FC<CreatePrivilegeGroupProps> = ({
+  onUpdate,
+  handleClose,
+  group = { group_name: '', privileges: [] },
+}) => {
+  const { t: userTrans } = useTranslation('user');
+  const { t: btnTrans } = useTranslation('btn');
+  const { t: warningTrans } = useTranslation('warning');
+  const [rbacOptions, setRbacOptions] = useState<PrivilegeGroup[]>([]);
+
+  const fetchRBAC = async () => {
+    const rbacOptions = await UserService.getAllPrivilegeGroups();
+
+    setRbacOptions(rbacOptions);
+  };
+
+  const isEditing = group.group_name !== '';
+
+  useEffect(() => {
+    fetchRBAC();
+  }, []);
+
+  const [form, setForm] = useState<CreatePrivilegeGroupParams>({
+    group_name: group.group_name,
+    privileges: group.privileges.map(p => p.name),
+  });
+
+  const checkedForm = useMemo(() => {
+    return formatForm(form);
+  }, [form]);
+  const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
+
+  const classes = useStyles();
+
+  const handleInputChange = (key: 'group_name', value: string) => {
+    setForm(v => {
+      const newFrom = { ...v, [key]: value };
+
+      return newFrom;
+    });
+  };
+
+  const createConfigs: ITextfieldConfig[] = [
+    {
+      label: userTrans('privilegeGroup'),
+      key: 'group_name',
+      onChange: (value: string) => handleInputChange('group_name', value),
+      variant: 'filled',
+      className: classes.input,
+      placeholder: userTrans('privilegeGroup'),
+      fullWidth: true,
+      validations: [
+        {
+          rule: 'require',
+          errorText: warningTrans('required', {
+            name: userTrans('privilegeGroup'),
+          }),
+        },
+      ],
+      defaultValue: form.group_name,
+      disabled: isEditing,
+    },
+  ];
+
+  const handleCreatePrivilegeGroup = async () => {
+    // privileges is an array of strings, should be unique
+    const newForm = {
+      ...form,
+      privileges: Array.from(new Set(form.privileges)),
+    };
+
+    if (!isEditing) {
+      await UserService.createPrivilegeGroup(newForm);
+    }
+
+    await UserService.updatePrivilegeGroup(newForm);
+
+    onUpdate({ data: newForm, isEditing: isEditing });
+  };
+
+  const onChange = (newSelection: any) => {
+    setForm(v => {
+      return { ...v, privileges: [...newSelection] };
+    });
+  };
+
+  const handleSelectAll = (
+    groupName: string,
+    privileges: string[],
+    isChecked: boolean
+  ) => {
+    const updatedPrivileges = isChecked
+      ? [...form.privileges, ...privileges]
+      : form.privileges.filter(p => !privileges.includes(p));
+
+    onChange(updatedPrivileges);
+  };
+
+  const isGroupAllSelected = (groupName: string, privileges: string[]) => {
+    return privileges.every(p => form.privileges.includes(p));
+  };
+
+  const isGroupPartialSelected = (groupName: string, privileges: string[]) => {
+    return (
+      privileges.some(p => form.privileges.includes(p)) &&
+      !isGroupAllSelected(groupName, privileges)
+    );
+  };
+
+  return (
+    <DialogTemplate
+      title={userTrans(
+        isEditing ? 'updatePrivilegeGroupTitle' : 'createPrivilegeGroupTitle'
+      )}
+      handleClose={handleClose}
+      confirmLabel={btnTrans(isEditing ? 'update' : 'create')}
+      handleConfirm={handleCreatePrivilegeGroup}
+      confirmDisabled={disabled}
+      dialogClass={classes.dialogWrapper}
+    >
+      <>
+        {createConfigs.map(v => (
+          <CustomInput
+            type="text"
+            textConfig={v}
+            checkValid={checkIsValid}
+            validInfo={validation}
+            key={v.label}
+          />
+        ))}
+        <Typography variant="h5" component="h5" className={classes.subTitle}>
+          {userTrans('privileges')}
+        </Typography>
+
+        {rbacOptions.map((grp, index) => {
+          const groupPrivileges = grp.privileges.map(p => p.name);
+          const isAllSelected = isGroupAllSelected(
+            grp.group_name,
+            groupPrivileges
+          );
+          const isPartialSelected = isGroupPartialSelected(
+            grp.group_name,
+            groupPrivileges
+          );
+
+          return (
+            <Accordion
+              key={`${grp.group_name}-${index}`}
+              className={classes.accordin}
+              elevation={0}
+            >
+              <AccordionSummary
+                className={classes.accordionSummary}
+                expandIcon={<ExpandMoreIcon />}
+                aria-controls={`${grp.group_name}-content`}
+                id={`${grp.group_name}-header`}
+                onClick={e => {
+                  if ((e.target as HTMLElement).closest('.MuiCheckbox-root')) {
+                    e.stopPropagation();
+                  }
+                }}
+              >
+                <Checkbox
+                  checked={isAllSelected}
+                  indeterminate={isPartialSelected}
+                  onChange={e =>
+                    handleSelectAll(
+                      grp.group_name,
+                      groupPrivileges,
+                      e.target.checked
+                    )
+                  }
+                  onClick={e => e.stopPropagation()}
+                  className="privilege-checkbox"
+                />
+                <Typography>
+                  {grp.group_name}(
+                  {
+                    new Set(
+                      form.privileges.filter(p => groupPrivileges.includes(p))
+                    ).size
+                  }
+                  /{new Set(groupPrivileges).size})
+                </Typography>
+              </AccordionSummary>
+              <AccordionDetails className={classes.accordionDetail}>
+                <PrivilegeGroupOptions
+                  options={groupPrivileges}
+                  selection={form.privileges}
+                  group_name={grp.group_name}
+                  onChange={onChange}
+                />
+              </AccordionDetails>
+            </Accordion>
+          );
+        })}
+      </>
+    </DialogTemplate>
+  );
+};
+
+export default UpdateRoleDialog;

+ 2 - 2
client/src/pages/user/UpdateRoleDialog.tsx → client/src/pages/user/dialogs/UpdateRoleDialog.tsx

@@ -6,15 +6,15 @@ import CustomInput from '@/components/customInput/CustomInput';
 import { useFormValidation } from '@/hooks';
 import { formatForm } from '@/utils';
 import { UserService } from '@/http';
-import PrivilegeOptions from './PrivilegeOptions';
 import { makeStyles } from '@mui/styles';
+import PrivilegeOptions from './PrivilegeOptions';
 import type { ITextfieldConfig } from '@/components/customInput/Types';
 import type {
   CreateRoleProps,
   CreateRoleParams,
   PrivilegeOptionsProps,
   RBACOptions,
-} from './Types';
+} from '../Types';
 
 const useStyles = makeStyles((theme: Theme) => ({
   input: {

+ 1 - 1
client/src/pages/user/Update.tsx → client/src/pages/user/dialogs/UpdateUserPassDialog.tsx

@@ -6,7 +6,7 @@ import CustomInput from '@/components/customInput/CustomInput';
 import { useFormValidation } from '@/hooks';
 import { formatForm } from '@/utils';
 import { makeStyles } from '@mui/styles';
-import type { UpdateUserParams, UpdateUserProps } from './Types';
+import type { UpdateUserParams, UpdateUserProps } from '../Types';
 import type { ITextfieldConfig } from '@/components/customInput/Types';
 
 const useStyles = makeStyles((theme: Theme) => ({

+ 0 - 0
client/src/pages/user/UpdateUserRole.tsx → client/src/pages/user/dialogs/UpdateUserRole.tsx


+ 2 - 1
client/src/router/Router.tsx

@@ -3,7 +3,7 @@ import { useContext } from 'react';
 import { authContext } from '@/context';
 import Databases from '@/pages/databases/Databases';
 import Connect from '@/pages/connect/Connect';
-import Users from '@/pages/user/Users';
+import Users from '@/pages/user/UsersAndRoles';
 import Index from '@/pages/index';
 import Search from '@/pages/search/VectorSearch';
 import System from '@/pages/system/SystemView';
@@ -34,6 +34,7 @@ const RouterComponent = () => {
             <>
               <Route path="users" element={<Users />} />
               <Route path="roles" element={<Users />} />
+              <Route path="privilege-groups" element={<Users />} />
               <Route path="system" element={<System />} />
             </>
           )}

+ 6 - 7
server/src/middleware/index.ts

@@ -32,18 +32,17 @@ export const ReqHeaderMiddleware = (
   }
 
   const bypassURLs = [`/api/v1/milvus/connect`, `/api/v1/milvus/version`];
+  const bypass = bypassURLs.indexOf(req.url) !== -1;
+  const hasClient = clientCache.get(milvusClientId);
 
-  if (
-    bypassURLs.indexOf(req.url) === -1 &&
-    milvusClientId &&
-    !clientCache.get(milvusClientId)
-  ) {
+  if (!bypass && !hasClient) {
     throw HttpErrors(
       HTTP_STATUS_CODE.FORBIDDEN,
-      'Can not find your connection, please check your connection settings.'
+      'Can not find your connection, please reconnect.'
     );
+  } else {
+    next();
   }
-  next();
 };
 
 export const TransformResMiddleware = (

+ 4 - 0
server/src/types/users.type.ts

@@ -1,7 +1,11 @@
 import {
   SelectRoleResponse,
   ListCredUsersResponse,
+  ListPrivilegeGroupsResponse,
+  PrivelegeGroup,
 } from '@zilliz/milvus2-sdk-node';
 
 export type Users = ListCredUsersResponse;
 export type UsersWithRoles = SelectRoleResponse;
+export type PrivilegeGroupsRes = ListPrivilegeGroupsResponse;
+export type PrivilegeGroup = PrivelegeGroup;

+ 41 - 14
server/src/users/dto.ts

@@ -1,41 +1,68 @@
-import { IsString } from 'class-validator';
+import { IsString, IsOptional } from 'class-validator';
 
 export class CreateUserDto {
-  @IsString()
+  @IsString({ message: 'username is required.' })
   readonly username: string;
 
-  @IsString()
+  @IsString({ message: 'password is required.' })
   readonly password: string;
 }
 
 export class CreateRoleDto {
-  @IsString()
+  @IsString({ message: 'roleName is required.' })
   readonly roleName: string;
 }
 
 export class UpdateUserDto {
-  @IsString()
+  @IsString({ message: 'username is required.' })
   readonly username: string;
 
-  @IsString()
+  @IsString({ message: 'oldPassword is required.' })
   readonly oldPassword: string;
 
-  @IsString()
+  @IsString({ message: 'newPassword is required.' })
   readonly newPassword: string;
 }
 
 export class AssignUserRoleDto {
-  @IsString()
-  readonly username: string;
-
-  @IsString({ each: true })
+  @IsString({ each: true, message: 'roles is required.' })
   readonly roles: string;
 }
 
 export class UnassignUserRoleDto {
-  @IsString()
-  readonly username: string;
+  @IsString({ message: 'roles is required.' })
+  readonly roleName: string;
+}
+
+// privilege group
+export class CreatePrivilegeGroupDto {
+  @IsString({ message: 'group_name is required.' })
+  readonly group_name: string;
+
+  @IsString({ message: 'privileges[] is required.', each: true })
+  readonly privileges: string[];
+}
+
+// get privilege group
+export class GetPrivilegeGroupDto {
+  @IsString({ message: 'group_name is required.' })
+  readonly group_name: string;
+}
+
+export class UpdatePrivilegeGroupDto {
+  @IsString({ message: 'privileges[] is required.', each: true })
+  readonly privileges: string[];
+}
+
+// grant/revoke privilege to role
+export class PrivilegeToRoleDto {
+  @IsString({ message: 'roleName is empty.' })
+  readonly role: string;
 
   @IsString()
-  readonly roleName: string;
+  @IsOptional()
+  readonly collection: string;
+
+  @IsString({ message: 'privilege is empty.' })
+  readonly privilege: string;
 }

+ 226 - 11
server/src/users/users.controller.ts

@@ -7,7 +7,11 @@ import {
   CreateRoleDto,
   AssignUserRoleDto,
   UnassignUserRoleDto,
+  UpdatePrivilegeGroupDto,
+  CreatePrivilegeGroupDto,
+  PrivilegeToRoleDto,
 } from './dto';
+import { OperateRolePrivilegeReq } from '@zilliz/milvus2-sdk-node';
 
 export class UserController {
   private router: Router;
@@ -45,6 +49,7 @@ export class UserController {
 
     // role
     this.router.get('/rbac', this.rbac.bind(this));
+    this.router.get('/privilegeGroups', this.allPrivilegeGroups.bind(this));
     this.router.get('/roles', this.getRoles.bind(this));
     this.router.post(
       '/roles',
@@ -58,6 +63,27 @@ export class UserController {
       this.updateRolePrivileges.bind(this)
     );
 
+    // privilege group
+    this.router.get('/privilege-groups', this.getPrivilegeGrps.bind(this));
+    this.router.get(
+      '/privilege-groups/:group_name',
+      this.getPrivilegeGrp.bind(this)
+    );
+    this.router.post(
+      '/privilege-groups',
+      dtoValidationMiddleware(CreatePrivilegeGroupDto),
+      this.createPrivilegeGrp.bind(this)
+    );
+    this.router.put(
+      '/privilege-groups/:group_name',
+      dtoValidationMiddleware(UpdatePrivilegeGroupDto),
+      this.updatePrivilegeGrp.bind(this)
+    );
+    this.router.delete(
+      '/privilege-groups/:group_name',
+      this.deletePrivilegeGrp.bind(this)
+    );
+
     return this.router;
   }
 
@@ -71,7 +97,11 @@ export class UserController {
     }
   }
 
-  async createUsers(req: Request, res: Response, next: NextFunction) {
+  async createUsers(
+    req: Request<{}, {}, CreateUserDto>,
+    res: Response,
+    next: NextFunction
+  ) {
     const { username, password } = req.body;
     try {
       const result = await this.userService.createUser(req.clientId, {
@@ -84,7 +114,11 @@ export class UserController {
     }
   }
 
-  async updateUsers(req: Request, res: Response, next: NextFunction) {
+  async updateUsers(
+    req: Request<{}, {}, UpdateUserDto>,
+    res: Response,
+    next: NextFunction
+  ) {
     const { username, oldPassword, newPassword } = req.body;
     try {
       const result = await this.userService.updateUser(req.clientId, {
@@ -98,7 +132,11 @@ export class UserController {
     }
   }
 
-  async deleteUser(req: Request, res: Response, next: NextFunction) {
+  async deleteUser(
+    req: Request<{ username: string }>,
+    res: Response,
+    next: NextFunction
+  ) {
     const { username } = req.params;
     try {
       const result = await this.userService.deleteUser(req.clientId, {
@@ -127,7 +165,11 @@ export class UserController {
     }
   }
 
-  async createRole(req: Request, res: Response, next: NextFunction) {
+  async createRole(
+    req: Request<{}, {}, CreateRoleDto>,
+    res: Response,
+    next: NextFunction
+  ) {
     const { roleName } = req.body;
     try {
       const result = await this.userService.createRole(req.clientId, {
@@ -139,7 +181,11 @@ export class UserController {
     }
   }
 
-  async deleteRole(req: Request, res: Response, next: NextFunction) {
+  async deleteRole(
+    req: Request<{ roleName: string }, {}, { force?: boolean }>,
+    res: Response,
+    next: NextFunction
+  ) {
     const { roleName } = req.params;
     const { force } = req.body;
 
@@ -158,9 +204,13 @@ export class UserController {
     }
   }
 
-  async updateUserRole(req: Request, res: Response, next: NextFunction) {
-    const { roles } = req.body;
+  async updateUserRole(
+    req: Request<{ username: string }, {}, AssignUserRoleDto>,
+    res: Response,
+    next: NextFunction
+  ) {
     const { username } = req.params;
+    const { roles } = req.body;
 
     const results = [];
 
@@ -197,9 +247,13 @@ export class UserController {
     }
   }
 
-  async unassignUserRole(req: Request, res: Response, next: NextFunction) {
-    const { roleName } = req.body;
+  async unassignUserRole(
+    req: Request<{ username: string }, {}, UnassignUserRoleDto>,
+    res: Response,
+    next: NextFunction
+  ) {
     const { username } = req.params;
+    const { roleName } = req.body;
 
     try {
       const result = await this.userService.unassignUserRole(req.clientId, {
@@ -221,7 +275,20 @@ export class UserController {
     }
   }
 
-  async listGrant(req: Request, res: Response, next: NextFunction) {
+  async allPrivilegeGroups(req: Request, res: Response, next: NextFunction) {
+    try {
+      const result = await this.userService.getAllPrivilegeGroups(req.clientId);
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async listGrant(
+    req: Request<{ roleName: string }>,
+    res: Response,
+    next: NextFunction
+  ) {
     const { roleName } = req.params;
     try {
       const result = await this.userService.listGrants(req.clientId, {
@@ -233,7 +300,15 @@ export class UserController {
     }
   }
 
-  async updateRolePrivileges(req: Request, res: Response, next: NextFunction) {
+  async updateRolePrivileges(
+    req: Request<
+      { roleName: string },
+      {},
+      { privileges: OperateRolePrivilegeReq[] }
+    >,
+    res: Response,
+    next: NextFunction
+  ) {
     const { privileges } = req.body;
     const { roleName } = req.params;
 
@@ -259,4 +334,144 @@ export class UserController {
       next(error);
     }
   }
+
+  async createPrivilegeGrp(
+    req: Request<{}, {}, CreatePrivilegeGroupDto>,
+    res: Response,
+    next: NextFunction
+  ) {
+    const { group_name, privileges } = req.body;
+    try {
+      // create the group
+      await this.userService.createPrivilegeGroup(req.clientId, {
+        group_name,
+      });
+
+      // add privileges to the group
+      const result = await this.userService.addPrivilegeToGroup(req.clientId, {
+        group_name,
+        priviliges: privileges,
+      });
+
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async deletePrivilegeGrp(
+    req: Request<{ group_name: string }>,
+    res: Response,
+    next: NextFunction
+  ) {
+    const { group_name } = req.params;
+    try {
+      const result = await this.userService.deletePrivilegeGroup(req.clientId, {
+        group_name,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async getPrivilegeGrp(
+    req: Request<{ group_name: string }>,
+    res: Response,
+    next: NextFunction
+  ) {
+    const { group_name } = req.params;
+    try {
+      const result = await this.userService.getPrivilegeGroup(req.clientId, {
+        group_name,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async getPrivilegeGrps(req: Request, res: Response, next: NextFunction) {
+    try {
+      const result = await this.userService.getPrivilegeGroups(req.clientId);
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async updatePrivilegeGrp(
+    req: Request<{ group_name: string }, {}, UpdatePrivilegeGroupDto>,
+    res: Response,
+    next: NextFunction
+  ) {
+    // get privilege group
+    const { group_name } = req.params;
+    const { privileges } = req.body;
+    // get existing group
+    const theGroup = await this.userService.getPrivilegeGroup(req.clientId, {
+      group_name: group_name,
+    });
+
+    // if no group found, return error
+    if (!theGroup) {
+      return next(new Error('Group not found'));
+    }
+
+    try {
+      // remove all privileges from the group
+      await this.userService.removePrivilegeFromGroup(req.clientId, {
+        group_name: group_name,
+        priviliges: theGroup.privileges.map(p => p.name),
+      });
+
+      // add new privileges to the group
+      const result = await this.userService.addPrivilegeToGroup(req.clientId, {
+        group_name: group_name,
+        priviliges: privileges,
+      });
+
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async assignRolePrivilege(
+    req: Request<{}, {}, PrivilegeToRoleDto>,
+    res: Response,
+    next: NextFunction
+  ) {
+    const { role, collection, privilege } = req.body;
+    try {
+      const result = await this.userService.grantPrivilegeV2(req.clientId, {
+        role: role,
+        collection_name: collection || '*',
+        db_name: req.db_name,
+        privilege: privilege,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async revokeRolePrivilege(
+    req: Request<{}, PrivilegeToRoleDto>,
+    res: Response,
+    next: NextFunction
+  ) {
+    const { role, collection, privilege } = req.body;
+    try {
+      const result = await this.userService.revokePrivilegeV2(req.clientId, {
+        role: role,
+        collection_name: collection || '*',
+        db_name: req.db_name,
+        privilege: privilege,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
 }

+ 135 - 0
server/src/users/users.service.ts

@@ -11,6 +11,8 @@ import {
   SelectUserReq,
   ListGrantsReq,
   OperateRolePrivilegeReq,
+  GrantPrivilegeV2Request,
+  RevokePrivilegeV2Request,
 } from '@zilliz/milvus2-sdk-node';
 import { throwErrorFromSDK } from '../utils/Error';
 import {
@@ -127,6 +129,40 @@ export class UserService {
     };
   }
 
+  async getAllPrivilegeGroups(clientId: string) {
+    const { milvusClient } = clientCache.get(clientId);
+    const privilegeGrps = await milvusClient.listPrivilegeGroups();
+
+    throwErrorFromSDK(privilegeGrps.status);
+
+    const defaultGrp = [
+      'ClusterAdmin',
+      'ClusterReadOnly',
+      'ClusterReadWrite',
+
+      'DatabaseAdmin',
+      'DatabaseReadOnly',
+      'DatabaseReadWrite',
+
+      'CollectionAdmin',
+      'CollectionReadOnly',
+      'CollectionReadWrite',
+    ];
+
+    // only show default groups
+    const groups = privilegeGrps.privilege_groups.filter(
+      g => defaultGrp.indexOf(g.group_name) !== -1
+    );
+
+    // sort groups by the order in defaultGrp
+    groups.sort(
+      (a, b) =>
+        defaultGrp.indexOf(a.group_name) - defaultGrp.indexOf(b.group_name)
+    );
+
+    return groups;
+  }
+
   async listGrants(clientId: string, data: ListGrantsReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.listGrants(data);
@@ -167,4 +203,103 @@ export class UserService {
       });
     }
   }
+
+  // create privilege group
+  async createPrivilegeGroup(clientId: string, data: { group_name: string }) {
+    const { milvusClient } = clientCache.get(clientId);
+
+    const res = await milvusClient.createPrivilegeGroup({
+      group_name: data.group_name,
+    });
+
+    throwErrorFromSDK(res);
+    return res;
+  }
+
+  // get privilege groups
+  async getPrivilegeGroups(clientId: string) {
+    const { milvusClient } = clientCache.get(clientId);
+
+    const res = await milvusClient.listPrivilegeGroups();
+
+    throwErrorFromSDK(res.status);
+
+    return res;
+  }
+
+  // get privilege groups and find the one with the name
+  async getPrivilegeGroup(clientId: string, data: { group_name: string }) {
+    const res = await this.getPrivilegeGroups(clientId);
+
+    const group = res.privilege_groups.find(
+      g => g.group_name === data.group_name
+    );
+
+    throwErrorFromSDK(res.status);
+    return group;
+  }
+
+  // delete privilege group
+  async deletePrivilegeGroup(clientId: string, data: { group_name: string }) {
+    const { milvusClient } = clientCache.get(clientId);
+
+    const res = await milvusClient.dropPrivilegeGroup({
+      group_name: data.group_name,
+    });
+
+    throwErrorFromSDK(res);
+    return res;
+  }
+
+  // update privilege group
+  async addPrivilegeToGroup(
+    clientId: string,
+    data: { group_name: string; priviliges: string[] }
+  ) {
+    const { milvusClient } = clientCache.get(clientId);
+
+    const res = await milvusClient.addPrivilegesToGroup({
+      group_name: data.group_name,
+      privileges: data.priviliges,
+    });
+
+    throwErrorFromSDK(res);
+    return res;
+  }
+
+  // remove privilege from group
+  async removePrivilegeFromGroup(
+    clientId: string,
+    data: { group_name: string; priviliges: string[] }
+  ) {
+    const { milvusClient } = clientCache.get(clientId);
+
+    const res = await milvusClient.removePrivilegesFromGroup({
+      group_name: data.group_name,
+      privileges: data.priviliges,
+    });
+
+    throwErrorFromSDK(res);
+    return res;
+  }
+
+  // grantPrivilegeV2
+  async grantPrivilegeV2(clientId: string, data: GrantPrivilegeV2Request) {
+    const { milvusClient } = clientCache.get(clientId);
+
+    const res = await milvusClient.grantPrivilegeV2(data);
+
+    throwErrorFromSDK(res);
+    return res;
+  }
+
+  // revokePrivilegeV2
+  async revokePrivilegeV2(clientId: string, data: RevokePrivilegeV2Request) {
+    const { milvusClient } = clientCache.get(clientId);
+
+    const res = await milvusClient.revokePrivilegeV2(data);
+
+    throwErrorFromSDK(res);
+    return res;
+  }
 }

+ 41 - 1
server/src/utils/Const.ts

@@ -83,6 +83,11 @@ export enum RbacObjects {
 
 // RBAC: collection privileges
 export enum CollectionPrivileges {
+  CreateCollection = 'CreateCollection',
+  DropCollection = 'DropCollection',
+  DescribeCollection = 'DescribeCollection',
+  ShowCollections = 'ShowCollections',
+  RenameCollection = 'RenameCollection',
   CreateIndex = 'CreateIndex',
   DropIndex = 'DropIndex',
   IndexDetail = 'IndexDetail',
@@ -105,6 +110,19 @@ export enum CollectionPrivileges {
   DropPartition = 'DropPartition',
   ShowPartitions = 'ShowPartitions',
   HasPartition = 'HasPartition',
+  FlushAll = 'FlushAll',
+  CreateAlias = 'CreateAlias',
+  DropAlias = 'DropAlias',
+  DescribeAlias = 'DescribeAlias',
+  ListAliases = 'ListAliases',
+}
+
+// RBAC: global privileges
+export enum DatabasePrivileges {
+  ListDatabases = 'ListDatabases',
+  DescribeDatabase = 'DescribeDatabase',
+  CreateDatabase = 'CreateDatabase',
+  DropDatabase = 'DropDatabase',
 }
 
 // RBAC: global privileges
@@ -135,17 +153,39 @@ export enum GlobalPrivileges {
   ListAliases = 'ListAliases',
 }
 
+// RBAC: resource group privileges
+export enum ResourceGroupPrivileges {
+  CreateResourceGroup = 'CreateResourceGroup',
+  DropResourceGroup = 'DropResourceGroup',
+  DescribeResourceGroup = 'DescribeResourceGroup',
+  ListResourceGroups = 'ListResourceGroups',
+  UpdateResourceGroups = 'UpdateResourceGroups',
+  TransferNode = 'TransferNode',
+  TransferReplica = 'TransferReplica',
+}
+
 // RBAC: user privileges
 export enum UserPrivileges {
   UpdateUser = 'UpdateUser',
   SelectUser = 'SelectUser',
+  SelectOwnership = 'SelectOwnership',
+  CreateOwnership = 'CreateOwnership',
+  DropOwnership = 'DropOwnership',
+  ManageOwnership = 'ManageOwnership',
+  CreatePrivilegeGroup = 'CreatePrivilegeGroup',
+  DropPrivilegeGroup = 'DropPrivilegeGroup',
+  ListPrivilegeGroups = 'ListPrivilegeGroups',
+  OperatePrivilegeGroup = 'OperatePrivilegeGroup',
+  RestoreRBAC = 'RestoreRBAC',
+  BackupRBAC = 'BackupRBAC',
 }
 
 // RBAC: all privileges
 export const Privileges = {
   ...CollectionPrivileges,
+  ...DatabasePrivileges,
+  ...ResourceGroupPrivileges,
   ...UserPrivileges,
-  ...GlobalPrivileges,
 };
 
 export enum LOADING_STATE {