Bläddra i källkod

feat: enhanced rbac GUI (#758)

* add collection selectors

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

* add database selector

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

* fix some logics

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

* WIP

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

* fix autocomplete selector

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

* update

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

* finish rbac UI

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

* show privilege count on collection option

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

* better performance

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

* bug fix

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

* bug fix

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

* fix server select role api

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

* part

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

* support indeterminate for privilege cate

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

* show privileges count for collection opitons

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

* show privileges count for db options

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

* part

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

* part

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

* fix user page

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

* add privileges on role page

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

* fix user page

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

* rename instance to cluster

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

* pv group part1

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

* pv group part2

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

* finish user and role pages and dialog

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

* update style

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

* fix home page

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

* clean

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

* adjust styles

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

* fix tips

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

* support duplicate role

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

* better permission info

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

---------

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
Signed-off-by: shanghaikid <jiangruiyi@gmail.com>
ryjiang 2 månader sedan
förälder
incheckning
2214806c6b
39 ändrade filer med 1612 tillägg och 822 borttagningar
  1. 8 3
      client/src/components/layout/GlobalEffect.tsx
  2. 62 0
      client/src/components/layout/Wrapper.tsx
  3. 0 191
      client/src/components/menu/CommunityBtn.tsx
  4. 2 2
      client/src/context/System.tsx
  5. 1 1
      client/src/context/Types.ts
  6. 1 1
      client/src/hooks/Navigation.tsx
  7. 6 0
      client/src/http/Collection.service.ts
  8. 9 11
      client/src/http/User.service.ts
  9. 2 0
      client/src/i18n/cn/common.ts
  10. 1 1
      client/src/i18n/cn/nav.ts
  11. 27 4
      client/src/i18n/cn/user.ts
  12. 2 0
      client/src/i18n/en/common.ts
  13. 1 1
      client/src/i18n/en/nav.ts
  14. 28 3
      client/src/i18n/en/user.ts
  15. 13 28
      client/src/pages/databases/collections/search/PartitionsSelector.tsx
  16. 16 6
      client/src/pages/user/PrivilegeGroups.tsx
  17. 154 49
      client/src/pages/user/Roles.tsx
  18. 33 37
      client/src/pages/user/Types.ts
  19. 70 64
      client/src/pages/user/User.tsx
  20. 545 0
      client/src/pages/user/dialogs/DBCollectionSelector.tsx
  21. 1 0
      client/src/pages/user/dialogs/PrivilegeGroupOptions.tsx
  22. 0 82
      client/src/pages/user/dialogs/PrivilegeOptions.tsx
  23. 0 1
      client/src/pages/user/dialogs/UpdatePrivilegeGroupDialog.tsx
  24. 107 106
      client/src/pages/user/dialogs/UpdateRoleDialog.tsx
  25. 2 2
      client/src/pages/user/dialogs/UpdateUserRole.tsx
  26. 77 0
      client/src/pages/user/dialogs/styles.ts
  27. 1 1
      server/package.json
  28. 27 0
      server/src/collections/collections.controller.ts
  29. 1 32
      server/src/collections/collections.service.ts
  30. 2 8
      server/src/database/databases.service.ts
  31. 10 4
      server/src/middleware/index.ts
  32. 8 2
      server/src/milvus/milvus.service.ts
  33. 0 7
      server/src/partitions/partitions.service.ts
  34. 40 0
      server/src/types/users.type.ts
  35. 230 35
      server/src/users/users.controller.ts
  36. 73 72
      server/src/users/users.service.ts
  37. 40 62
      server/src/utils/Const.ts
  38. 2 2
      server/src/utils/Error.ts
  39. 10 4
      server/yarn.lock

+ 8 - 3
client/src/components/layout/GlobalEffect.tsx

@@ -41,23 +41,28 @@ const GlobalEffect = ({ children }: { children: React.ReactNode }) => {
         },
         },
         error => {
         error => {
           const { response } = error;
           const { response } = error;
+          let messageType = 'error';
+
           if (response) {
           if (response) {
             switch (response.status) {
             switch (response.status) {
               case HTTP_STATUS_CODE.UNAUTHORIZED:
               case HTTP_STATUS_CODE.UNAUTHORIZED:
-              case HTTP_STATUS_CODE.FORBIDDEN:
                 setTimeout(() => logout(true), 1000);
                 setTimeout(() => logout(true), 1000);
                 break;
                 break;
+
+              case HTTP_STATUS_CODE.FORBIDDEN:
+                messageType = 'warning';
+                break;
               default:
               default:
                 break;
                 break;
             }
             }
             const errorMessage = response.data?.message;
             const errorMessage = response.data?.message;
             if (errorMessage) {
             if (errorMessage) {
-              openSnackBar(errorMessage, 'error');
+              openSnackBar(errorMessage, messageType);
               return Promise.reject(error);
               return Promise.reject(error);
             }
             }
           }
           }
           // Handle other error cases
           // Handle other error cases
-          openSnackBar(error.message, 'error');
+          openSnackBar(error.message, messageType);
           return Promise.reject(error);
           return Promise.reject(error);
         }
         }
       );
       );

+ 62 - 0
client/src/components/layout/Wrapper.tsx

@@ -0,0 +1,62 @@
+import { makeStyles } from '@mui/styles';
+import { Theme } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+
+interface WrapperProps {
+  hasPermission?: boolean;
+  children?: React.ReactNode;
+  className?: string;
+}
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    width: '100%',
+    height: '100%',
+    position: 'relative',
+  },
+  overlay: {
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    bottom: 0,
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'center',
+    backgroundColor: theme.palette.background.default,
+    zIndex: 1000,
+  },
+  message: {
+    fontSize: 14,
+    color: theme.palette.text.primary,
+    textAlign: 'center',
+    fontStyle: 'italic',
+  },
+}));
+
+const Wrapper = ({
+  hasPermission = true,
+  children,
+  className,
+}: WrapperProps) => {
+  // styles
+  const classes = useStyles();
+
+  // i18n
+  const { t: commonTrans } = useTranslation();
+
+  return (
+    <div className={`${classes.wrapper} ${className}`}>
+      {children}
+      {!hasPermission && (
+        <div className={classes.overlay}>
+          <div className={classes.message}>
+            {commonTrans('noPermissionTip')}
+          </div>
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default Wrapper;

+ 0 - 191
client/src/components/menu/CommunityBtn.tsx

@@ -1,191 +0,0 @@
-import React from 'react';
-import { useTranslation } from 'react-i18next';
-import Button from '@mui/material/Button';
-import SvgIcon from '@mui/material/SvgIcon';
-import { Theme, Link } from '@mui/material';
-import ChevronRightIcon from '@mui/icons-material/ChevronRight';
-import GitHubIcon from '@mui/icons-material/GitHub';
-import peopleIcon from '@/assets/icons/people.svg?react';
-import qrcodePath from '@/assets/imgs/wechat_qrcode.jpeg';
-import discordIcon from '@/assets/icons/discord.svg?react';
-import { makeStyles } from '@mui/styles';
-
-const GITHUB_LINK = 'https://github.com/milvus-io/milvus/discussions';
-const DISCORD_LINK = 'https://discord.com/invite/8uyFbECzPX';
-
-const getStyles = makeStyles((theme: Theme) => ({
-  root: {
-    bottom: theme.spacing(2),
-    position: 'absolute',
-    right: theme.spacing(1),
-    width: theme.spacing(5),
-    zIndex: 3,
-  },
-  menuBtn: {
-    border: '1px solid #E9E9ED',
-    borderRadius: '50%',
-    bottom: 0,
-    boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
-    height: theme.spacing(5),
-    minWidth: 'auto',
-    padding: 0,
-    position: 'absolute',
-    width: theme.spacing(5),
-  },
-  chevronIcon: {
-    transform: 'rotateZ(90deg)',
-    fill: theme.palette.primary.main,
-  },
-  container: {
-    bottom: theme.spacing(7),
-    position: 'absolute',
-    width: '360px',
-    overflow: 'hidden',
-  },
-  head: {
-    backgroundColor: theme.palette.primary.main,
-    padding: theme.spacing(1.5, 2.5),
-    color: '#fff',
-    borderTopLeftRadius: theme.spacing(1),
-    borderTopRightRadius: theme.spacing(1),
-  },
-  title: {
-    fontWeight: 700,
-    fontSize: theme.spacing(2),
-    lineHeight: theme.spacing(3),
-    letterSpacing: '-0.01em',
-  },
-  titleDesc: {
-    color: '#f0f4f9',
-    fontSize: theme.spacing(1.5),
-    lineHeight: theme.spacing(2),
-  },
-  body: {
-    backgroundColor: '#fff',
-    border: '1px solid #e9e9e9',
-    borderTop: 0,
-    borderBottomRightRadius: theme.spacing(1),
-    borderBottomLeftRadius: theme.spacing(1),
-    padding: theme.spacing(3),
-  },
-  block: {
-    border: '1px solid #f4f4f4',
-    boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
-    marginBottom: theme.spacing(3),
-    padding: theme.spacing(2),
-  },
-  contentTitle: {
-    fontWeight: 500,
-    fontSize: theme.spacing(1.75),
-    lineHeight: theme.spacing(2.5),
-  },
-  contentDesc: {
-    fontSize: theme.spacing(1.5),
-    lineHeight: theme.spacing(2.5),
-    color: theme.palette.text.secondary,
-    marginBottom: theme.spacing(1),
-  },
-  contentLink: {
-    display: 'block',
-    fontSize: theme.spacing(1.5),
-    lineHeight: theme.spacing(2.5),
-    letterSpacing: '-0.01em',
-    color: theme.palette.primary.main,
-  },
-  qrImg: {
-    display: 'block',
-    margin: '0 auto',
-    width: theme.spacing(10),
-  },
-  textCenter: {
-    textAlign: 'center',
-  },
-  icon: {
-    marginTop: theme.spacing(2),
-    width: theme.spacing(2.5),
-    height: theme.spacing(2.5),
-  },
-}));
-
-const CommunityBtn = (props: any) => {
-  const [open, setOpen] = React.useState<boolean>(false);
-  const classes = getStyles();
-  const { t } = useTranslation();
-  const communityTrans: { [key in string]: string } = t('community');
-
-  return (
-    <div className={classes.root}>
-      {open && (
-        <div className={classes.container}>
-          <div className={classes.head}>
-            <div className={classes.title}>{communityTrans.hi}</div>
-            <div className={classes.titleDesc}>{communityTrans.growing}</div>
-          </div>
-          <div className={classes.body}>
-            <div className={classes.block}>
-              <div className={`${classes.contentTitle} ${classes.textCenter}`}>
-                {communityTrans.question}
-              </div>
-              <div className={`${classes.contentDesc} ${classes.textCenter}`}>
-                {communityTrans.qr}
-              </div>
-              <img className={classes.qrImg} src={qrcodePath} alt="qrcode" />
-            </div>
-            <div className={classes.block}>
-              <div className={`${classes.contentTitle} ${classes.textCenter}`}>
-                {communityTrans.more}
-              </div>
-
-              <SvgIcon
-                viewBox="0 0 24 24"
-                component={discordIcon}
-                className={classes.icon}
-              />
-              <div className={classes.contentDesc}>{communityTrans.join}</div>
-              <Link
-                classes={{ root: classes.contentLink }}
-                href={DISCORD_LINK}
-                underline="always"
-                target="_blank"
-                rel="noopener"
-              >
-                {DISCORD_LINK}
-              </Link>
-
-              <SvgIcon
-                viewBox="0 0 24 24"
-                component={GitHubIcon}
-                className={classes.icon}
-              />
-              <div className={classes.contentDesc}>{communityTrans.get}</div>
-              <Link
-                classes={{ root: classes.contentLink }}
-                href={GITHUB_LINK}
-                underline="always"
-                target="_blank"
-                rel="noopener"
-              >
-                {GITHUB_LINK}
-              </Link>
-            </div>
-          </div>
-        </div>
-      )}
-      <Button
-        className={classes.menuBtn}
-        aria-haspopup="true"
-        onClick={() => {
-          setOpen(!open);
-        }}
-      >
-        {open ? (
-          <ChevronRightIcon className={classes.chevronIcon} />
-        ) : (
-          <SvgIcon viewBox="0 0 24 24" component={peopleIcon} />
-        )}
-      </Button>
-    </div>
-  );
-};
-
-export default CommunityBtn;

+ 2 - 2
client/src/context/System.tsx

@@ -54,8 +54,8 @@ export const SystemProvider = (props: { children: React.ReactNode }) => {
       const systemInfo = rootCoord.infos.system_info;
       const systemInfo = rootCoord.infos.system_info;
 
 
       const data = {
       const data = {
-        users: users.usernames,
-        roles: roles.results,
+        users: users,
+        roles: roles,
         queryNodes,
         queryNodes,
         dataNodes,
         dataNodes,
         indexNodes,
         indexNodes,

+ 1 - 1
client/src/context/Types.ts

@@ -60,7 +60,7 @@ export type SnackBarType = {
 
 
 export type OpenSnackBarType = (
 export type OpenSnackBarType = (
   message: string | ReactElement,
   message: string | ReactElement,
-  type?: 'error' | 'info' | 'success' | 'warning',
+  type?: 'error' | 'info' | 'success' | 'warning' | string,
   autoHideDuration?: number | null,
   autoHideDuration?: number | null,
   position?: {
   position?: {
     horizontal: 'center' | 'left' | 'right';
     horizontal: 'center' | 'left' | 'right';

+ 1 - 1
client/src/hooks/Navigation.tsx

@@ -71,7 +71,7 @@ export const useNavigationHook = (
         const navInfo: NavInfo = {
         const navInfo: NavInfo = {
           navTitle: navTrans('user'),
           navTitle: navTrans('user'),
           backPath: '',
           backPath: '',
-          showDatabaseSelector: true,
+          showDatabaseSelector: false,
         };
         };
         setNavInfo(navInfo);
         setNavInfo(navInfo);
         break;
         break;

+ 6 - 0
client/src/http/Collection.service.ts

@@ -24,6 +24,12 @@ export class CollectionService extends BaseModel {
     return super.findAll({ path: `/collections`, params: data || {} });
     return super.findAll({ path: `/collections`, params: data || {} });
   }
   }
 
 
+  static getCollectionsNames(data?: {
+    db_name: string;
+  }): Promise<string[]> {
+    return super.findAll({ path: `/collections/names`, params: data || {} });
+  }
+
   static describeCollectionUnformatted(collectionName: string) {
   static describeCollectionUnformatted(collectionName: string) {
     return super.search({
     return super.search({
       path: `/collections/${collectionName}/unformatted`,
       path: `/collections/${collectionName}/unformatted`,

+ 9 - 11
client/src/http/User.service.ts

@@ -1,9 +1,10 @@
 import BaseModel from './BaseModel';
 import BaseModel from './BaseModel';
 import type {
 import type {
-  Users,
-  UsersWithRoles,
+  RolesWithPrivileges,
   PrivilegeGroup,
   PrivilegeGroup,
   PrivilegeGroupsRes,
   PrivilegeGroupsRes,
+  UserWithRoles,
+  RBACOptions,
 } from '@server/types';
 } from '@server/types';
 import type {
 import type {
   CreateUserParams,
   CreateUserParams,
@@ -19,12 +20,15 @@ import type {
 export class UserService extends BaseModel {
 export class UserService extends BaseModel {
   // get user data
   // get user data
   static getUsers() {
   static getUsers() {
-    return super.search<Users>({ path: `/users`, params: {} });
+    return super.search<UserWithRoles[]>({ path: `/users`, params: {} });
   }
   }
 
 
   // get all roles
   // get all roles
   static getRoles() {
   static getRoles() {
-    return super.search<UsersWithRoles>({ path: `/users/roles`, params: {} });
+    return super.search<RolesWithPrivileges[]>({
+      path: `/users/roles`,
+      params: {},
+    });
   }
   }
 
 
   // create user
   // create user
@@ -89,13 +93,7 @@ export class UserService extends BaseModel {
     return super.search({
     return super.search({
       path: `/users/rbac`,
       path: `/users/rbac`,
       params: {},
       params: {},
-    }) as Promise<{
-      GlobalPrivileges: Record<string, unknown>;
-      CollectionPrivileges: Record<string, unknown>;
-      RbacObjects: Record<string, unknown>;
-      UserPrivileges: Record<string, unknown>;
-      Privileges: Record<string, unknown>;
-    }>;
+    }) as Promise<RBACOptions>;
   }
   }
   // get privilege groups
   // get privilege groups
   static getPrivilegeGroups() {
   static getPrivilegeGroups() {

+ 2 - 0
client/src/i18n/cn/common.ts

@@ -85,6 +85,8 @@ const commonTrans = {
   memory: '内存',
   memory: '内存',
   yes: '是',
   yes: '是',
   no: '否',
   no: '否',
+
+  noPermissionTip: '您没有权限访问此页面。',
 };
 };
 
 
 export default commonTrans;
 export default commonTrans;

+ 1 - 1
client/src/i18n/cn/nav.ts

@@ -5,7 +5,7 @@ const navTrans = {
   console: '搜索控制台',
   console: '搜索控制台',
   search: '向量搜索',
   search: '向量搜索',
   system: '系统视图',
   system: '系统视图',
-  user: '用户',
+  user: '用户和角色',
   dbAdmin: '数据库管理',
   dbAdmin: '数据库管理',
   database: '数据库',
   database: '数据库',
 };
 };

+ 27 - 4
client/src/i18n/cn/user.ts

@@ -1,5 +1,3 @@
-import PrivilegeGroups from '@/pages/user/PrivilegeGroups';
-
 const userTrans = {
 const userTrans = {
   createTitle: '创建用户',
   createTitle: '创建用户',
   updateTitle: '更新Milvus用户',
   updateTitle: '更新Milvus用户',
@@ -7,21 +5,25 @@ const userTrans = {
   user: '用户',
   user: '用户',
   users: '用户们',
   users: '用户们',
   deleteWarning: '您正在尝试删除用户。此操作无法撤销。',
   deleteWarning: '您正在尝试删除用户。此操作无法撤销。',
+  deleteRoleWarning: '您正在尝试删除角色。此操作无法撤销。',
   oldPassword: '当前密码',
   oldPassword: '当前密码',
   newPassword: '新密码',
   newPassword: '新密码',
   confirmPassword: '确认密码',
   confirmPassword: '确认密码',
   update: '更新密码',
   update: '更新密码',
   isNotSame: '与新密码不同',
   isNotSame: '与新密码不同',
-  deleteTip: '请至少选择一个要删除的项目,不能删除root用户。',
+  deleteTip: '请至少选择一个要删除的用户,不能删除root用户。',
+  deleteRoleTip: '请至少选择一个要删除的角色,不能删除admin/public 角色。',
+  editPassword: '修改密码',
 
 
   // role
   // role
-  deleteEditRoleTip: 'root角色不可编辑。',
+  deleteEditRoleTip: '请选择一个角色,并且root角色不可编辑。',
   disableEditRolePrivilegeTip: 'admin和public角色不可编辑。',
   disableEditRolePrivilegeTip: 'admin和public角色不可编辑。',
 
 
   role: '角色',
   role: '角色',
   editRole: '编辑角色',
   editRole: '编辑角色',
   roles: '角色',
   roles: '角色',
   createRoleTitle: '创建角色',
   createRoleTitle: '创建角色',
+  dupicateRoleTitle: '复制角色',
   updateRolePrivilegeTitle: '更新角色',
   updateRolePrivilegeTitle: '更新角色',
   updateRoleSuccess: '用户角色',
   updateRoleSuccess: '用户角色',
   type: '类型',
   type: '类型',
@@ -42,6 +44,27 @@ const userTrans = {
   deletePrivilegGroupWarning: '您正在尝试删除权限组,请确保没有角色与其绑定。',
   deletePrivilegGroupWarning: '您正在尝试删除权限组,请确保没有角色与其绑定。',
   createPrivilegeGroupTitle: '创建权限组',
   createPrivilegeGroupTitle: '创建权限组',
   updatePrivilegeGroupTitle: '更新权限组',
   updatePrivilegeGroupTitle: '更新权限组',
+  allCollections: '所有集合',
+  allDatabases: '所有Database 及所有集合',
+
+  collections: 'Collection',
+  collection: 'Collection',
+  databases: 'Database',
+  database: 'Database',
+  cluster: '集群',
+
+  DatabasePrivileges: 'Database 相关权限',
+  CollectionPrivileges: 'Collection 相关权限',
+  PartitionPrivileges: 'Partition 相关权限',
+  IndexPrivileges: 'Index 相关权限',
+  EntityPrivileges: 'Entity 相关权限',
+  ResourceManagementPrivileges: '资源管理相关权限',
+  RBACPrivileges: 'RBAC相关权限',
+
+  CollectionPrivilegeGroups: '内置权限组',
+  DatabasePrivilegeGroups: '内置权限组',
+  ClusterPrivilegeGroups: '内置权限组',
+  CustomPrivilegeGroups: '用户定义权限组',
 };
 };
 
 
 export default userTrans;
 export default userTrans;

+ 2 - 0
client/src/i18n/en/common.ts

@@ -86,6 +86,8 @@ const commonTrans = {
   memory: 'Memory',
   memory: 'Memory',
   yes: 'Yes',
   yes: 'Yes',
   no: 'No',
   no: 'No',
+
+  noPermissionTip: `You don't have permission to access this content.`,
 };
 };
 
 
 export default commonTrans;
 export default commonTrans;

+ 1 - 1
client/src/i18n/en/nav.ts

@@ -5,7 +5,7 @@ const navTrans = {
   console: 'Search Console',
   console: 'Search Console',
   search: 'Vector Search',
   search: 'Vector Search',
   system: 'System View',
   system: 'System View',
-  user: 'User',
+  user: 'User and Role',
   dbAdmin: 'DB Management',
   dbAdmin: 'DB Management',
   database: 'Database',
   database: 'Database',
 };
 };

+ 28 - 3
client/src/i18n/en/user.ts

@@ -1,5 +1,3 @@
-import { create } from "domain";
-
 const userTrans = {
 const userTrans = {
   createTitle: 'Create User',
   createTitle: 'Create User',
   updateTitle: 'Update Milvus User',
   updateTitle: 'Update Milvus User',
@@ -7,6 +5,8 @@ const userTrans = {
   user: 'User',
   user: 'User',
   users: 'Users',
   users: 'Users',
   deleteWarning: 'You are trying to drop user. This action cannot be undone.',
   deleteWarning: 'You are trying to drop user. This action cannot be undone.',
+  deleteRoleWarning:
+    'You are trying to drop role. This action cannot be undone.',
   oldPassword: 'Current Password',
   oldPassword: 'Current Password',
   newPassword: 'New Password',
   newPassword: 'New Password',
   confirmPassword: 'Confirm Password',
   confirmPassword: 'Confirm Password',
@@ -14,15 +14,19 @@ const userTrans = {
   isNotSame: 'Not same as new password',
   isNotSame: 'Not same as new password',
   deleteTip:
   deleteTip:
     'Please select at least one item to drop and the root user can not be dropped.',
     'Please select at least one item to drop and the root user can not be dropped.',
+  deleteRoleTip:
+    'Please select at least one item to drop and the admin/public role can not be dropped.',
+  editPassword: 'Edit Password',
 
 
   // role
   // role
-  deleteEditRoleTip: 'root role is not editable.',
+  deleteEditRoleTip: 'Please select one user to edit, root is not editable.',
   disableEditRolePrivilegeTip: 'admin and public role are not editable.',
   disableEditRolePrivilegeTip: 'admin and public role are not editable.',
 
 
   role: 'Role',
   role: 'Role',
   editRole: 'Edit Role',
   editRole: 'Edit Role',
   roles: 'Roles',
   roles: 'Roles',
   createRoleTitle: 'Create Role',
   createRoleTitle: 'Create Role',
+  dupicateRoleTitle: 'Duplicate Role',
   updateRolePrivilegeTitle: 'Update Role',
   updateRolePrivilegeTitle: 'Update Role',
   updateRoleSuccess: 'User Role',
   updateRoleSuccess: 'User Role',
   type: 'Type',
   type: 'Type',
@@ -44,6 +48,27 @@ const userTrans = {
     'You are trying to drop the privilege group, please make sure no role is bound to it.',
     'You are trying to drop the privilege group, please make sure no role is bound to it.',
   createPrivilegeGroupTitle: 'Create Privilege Group',
   createPrivilegeGroupTitle: 'Create Privilege Group',
   updatePrivilegeGroupTitle: 'Update Privilege Group',
   updatePrivilegeGroupTitle: 'Update Privilege Group',
+  allCollections: 'All Collections',
+  allDatabases: 'All Databases',
+
+  collections: 'Collections',
+  collection: 'Collection',
+  databases: 'Databases',
+  database: 'Database',
+  cluster: 'Cluster',
+
+  DatabasePrivileges: 'Database Privileges',
+  CollectionPrivileges: 'Collection Privileges',
+  PartitionPrivileges: 'Partition Privileges',
+  IndexPrivileges: 'Index Privileges',
+  EntityPrivileges: 'Entity Privileges',
+  ResourceManagementPrivileges: 'Resource Management Privileges',
+  RBACPrivileges: 'RBAC Privileges',
+
+  CollectionPrivilegeGroups: 'Built-in Groups',
+  DatabasePrivilegeGroups: 'Built-in Groups',
+  ClusterPrivilegeGroups: 'Built-in Groups',
+  CustomPrivilegeGroups: 'User-defined Groups',
 };
 };
 
 
 export default userTrans;
 export default userTrans;

+ 13 - 28
client/src/pages/databases/collections/search/PartitionsSelector.tsx

@@ -1,9 +1,8 @@
 import { useState } from 'react';
 import { useState } from 'react';
+import { TextField } from '@mui/material';
 import Autocomplete from '@mui/material/Autocomplete';
 import Autocomplete from '@mui/material/Autocomplete';
-import CircularProgress from '@mui/material/CircularProgress';
 import { PartitionService } from '@/http';
 import { PartitionService } from '@/http';
 import type { PartitionData } from '@server/types';
 import type { PartitionData } from '@server/types';
-import CustomInput from '@/components/customInput/CustomInput';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 interface PartitionsSelectorProps {
 interface PartitionsSelectorProps {
@@ -15,28 +14,24 @@ interface PartitionsSelectorProps {
 export default function PartitionsSelector(props: PartitionsSelectorProps) {
 export default function PartitionsSelector(props: PartitionsSelectorProps) {
   // i18n
   // i18n
   const { t: searchTrans } = useTranslation('search');
   const { t: searchTrans } = useTranslation('search');
-  // default loading
-  const DEFAULT_LOADING_OPTIONS: readonly PartitionData[] = [
-    { name: searchTrans('loading'), id: -1, rowCount: -1, createdTime: '' },
-  ];
 
 
   // props
   // props
   const { collectionName, selected, setSelected } = props;
   const { collectionName, selected, setSelected } = props;
   // state
   // state
   const [open, setOpen] = useState(false);
   const [open, setOpen] = useState(false);
-  const [options, setOptions] = useState<readonly PartitionData[]>(
-    DEFAULT_LOADING_OPTIONS
-  );
+  const [options, setOptions] = useState<readonly PartitionData[]>([]);
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
 
 
   const handleOpen = () => {
   const handleOpen = () => {
     setOpen(true);
     setOpen(true);
+    setLoading(true);
     (async () => {
     (async () => {
       try {
       try {
         const res = await PartitionService.getPartitions(collectionName);
         const res = await PartitionService.getPartitions(collectionName);
-        setLoading(false);
         setOptions([...res]);
         setOptions([...res]);
       } catch (err) {
       } catch (err) {
+        console.error(err);
+      } finally {
         setLoading(false);
         setLoading(false);
       }
       }
     })();
     })();
@@ -44,7 +39,6 @@ export default function PartitionsSelector(props: PartitionsSelectorProps) {
 
 
   const handleClose = () => {
   const handleClose = () => {
     setOpen(false);
     setOpen(false);
-    setOptions(DEFAULT_LOADING_OPTIONS);
   };
   };
 
 
   return (
   return (
@@ -66,24 +60,15 @@ export default function PartitionsSelector(props: PartitionsSelectorProps) {
       getOptionLabel={option => (option && option.name) || ''}
       getOptionLabel={option => (option && option.name) || ''}
       options={options}
       options={options}
       loading={loading}
       loading={loading}
+      noOptionsText={
+        loading ? searchTrans('loading') : searchTrans('noOptions')
+      }
       renderInput={params => {
       renderInput={params => {
-        return loading ? (
-          <CircularProgress color="inherit" size={20} />
-        ) : (
-          <CustomInput
-            textConfig={{
-              ...params,
-              label: searchTrans('partitionFilter'),
-              key: 'partitionFilter',
-              className: 'input',
-              value: params.inputProps.value,
-              disabled: false,
-              variant: 'filled',
-              required: false,
-              InputLabelProps: { shrink: true },
-            }}
-            checkValid={() => true}
-            type="text"
+        return (
+          <TextField
+            {...params}
+            label={searchTrans('partitionFilter')}
+            variant="filled"
           />
           />
         );
         );
       }}
       }}

+ 16 - 6
client/src/pages/user/PrivilegeGroups.tsx

@@ -6,6 +6,7 @@ import { rootContext } from '@/context';
 import { useNavigationHook, usePaginationHook } from '@/hooks';
 import { useNavigationHook, usePaginationHook } from '@/hooks';
 import AttuGrid from '@/components/grid/Grid';
 import AttuGrid from '@/components/grid/Grid';
 import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
 import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
+import Wrapper from '@/components/layout/Wrapper';
 import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
 import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
 import { ALL_ROUTER_TYPES } from '@/router/consts';
 import { ALL_ROUTER_TYPES } from '@/router/consts';
 import UpdatePrivilegeGroupDialog from './dialogs/UpdatePrivilegeGroupDialog';
 import UpdatePrivilegeGroupDialog from './dialogs/UpdatePrivilegeGroupDialog';
@@ -24,13 +25,18 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 
 const PrivilegeGroups = () => {
 const PrivilegeGroups = () => {
   useNavigationHook(ALL_ROUTER_TYPES.USER);
   useNavigationHook(ALL_ROUTER_TYPES.USER);
+  // styles
   const classes = useStyles();
   const classes = useStyles();
 
 
+  // ui states
   const [groups, setGroups] = useState<PrivilegeGroup[]>([]);
   const [groups, setGroups] = useState<PrivilegeGroup[]>([]);
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
   const [selectedGroups, setSelectedGroups] = useState<PrivilegeGroup[]>([]);
   const [selectedGroups, setSelectedGroups] = useState<PrivilegeGroup[]>([]);
+  const [hasPermission, setHasPermission] = useState(true);
+  // context
   const { setDialog, handleCloseDialog, openSnackBar } =
   const { setDialog, handleCloseDialog, openSnackBar } =
     useContext(rootContext);
     useContext(rootContext);
+  // i18n
   const { t: successTrans } = useTranslation('success');
   const { t: successTrans } = useTranslation('success');
   const { t: userTrans } = useTranslation('user');
   const { t: userTrans } = useTranslation('user');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
@@ -38,10 +44,14 @@ const PrivilegeGroups = () => {
 
 
   const fetchGroups = async () => {
   const fetchGroups = async () => {
     setLoading(true);
     setLoading(true);
-    const res = await UserService.getPrivilegeGroups();
-    setGroups(res.privilege_groups);
-
-    setLoading(false);
+    try {
+      const res = await UserService.getPrivilegeGroups();
+      setGroups(res.privilege_groups);
+    } catch (error) {
+      setHasPermission(false);
+    } finally {
+      setLoading(false);
+    }
   };
   };
 
 
   const onUpdate = async (data: { isEditing: boolean }) => {
   const onUpdate = async (data: { isEditing: boolean }) => {
@@ -198,7 +208,7 @@ const PrivilegeGroups = () => {
   };
   };
 
 
   return (
   return (
-    <div className={classes.wrapper}>
+    <Wrapper className={classes.wrapper} hasPermission={hasPermission}>
       <AttuGrid
       <AttuGrid
         toolbarConfigs={[]}
         toolbarConfigs={[]}
         colDefinitions={colDefinitions}
         colDefinitions={colDefinitions}
@@ -220,7 +230,7 @@ const PrivilegeGroups = () => {
         handleSort={handleGridSort}
         handleSort={handleGridSort}
         labelDisplayedRows={getLabelDisplayedRows(userTrans('privilegeGroups'))}
         labelDisplayedRows={getLabelDisplayedRows(userTrans('privilegeGroups'))}
       />
       />
-    </div>
+    </Wrapper>
   );
   );
 };
 };
 
 

+ 154 - 49
client/src/pages/user/Roles.tsx

@@ -13,8 +13,14 @@ import type {
   ColDefinitionsType,
   ColDefinitionsType,
   ToolBarConfig,
   ToolBarConfig,
 } from '@/components/grid/Types';
 } from '@/components/grid/Types';
-import type { DeleteRoleParams, RoleData } from './Types';
+import Wrapper from '@/components/layout/Wrapper';
+import type { DeleteRoleParams } from './Types';
 import { getLabelDisplayedRows } from '@/pages/search/Utils';
 import { getLabelDisplayedRows } from '@/pages/search/Utils';
+import type {
+  RolesWithPrivileges,
+  RBACOptions,
+  DBCollectionsPrivileges,
+} from '@server/types';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
   wrapper: {
@@ -27,35 +33,45 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 
 const Roles = () => {
 const Roles = () => {
   useNavigationHook(ALL_ROUTER_TYPES.USER);
   useNavigationHook(ALL_ROUTER_TYPES.USER);
+  // styles
   const classes = useStyles();
   const classes = useStyles();
+  // context
   const { database } = useContext(dataContext);
   const { database } = useContext(dataContext);
-  const [loading, setLoading] = useState(false);
-
-  const [roles, setRoles] = useState<RoleData[]>([]);
-  const [selectedRole, setSelectedRole] = useState<RoleData[]>([]);
   const { setDialog, handleCloseDialog, openSnackBar } =
   const { setDialog, handleCloseDialog, openSnackBar } =
     useContext(rootContext);
     useContext(rootContext);
+  // ui states
+  const [loading, setLoading] = useState(false);
+  const [roles, setRoles] = useState<RolesWithPrivileges[]>([]);
+  const [rbacOptions, setRbacOptions] = useState<RBACOptions>(
+    {} as RBACOptions
+  );
+  const [selectedRole, setSelectedRole] = useState<RolesWithPrivileges[]>([]);
+  const [hasPermission, setHasPermission] = useState(true);
+
+  // i18n
   const { t: successTrans } = useTranslation('success');
   const { t: successTrans } = useTranslation('success');
   const { t: userTrans } = useTranslation('user');
   const { t: userTrans } = useTranslation('user');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: dialogTrans } = useTranslation('dialog');
 
 
   const fetchRoles = async () => {
   const fetchRoles = async () => {
-    const roles = await UserService.getRoles();
-    setSelectedRole([]);
-
-    setRoles(
-      roles.results.map(v => ({
-        name: v.role.name,
-        privilegeContent: v,
-        privileges: v.entities.map(e => ({
-          roleName: v.role.name,
-          object: e.object.name,
-          objectName: e.object_name,
-          privilegeName: e.grantor.privilege.name,
-        })),
-      }))
-    );
+    setLoading(true);
+
+    try {
+      const [roles, rbacs] = await Promise.all([
+        UserService.getRoles(),
+        UserService.getRBAC(),
+      ]);
+
+      setSelectedRole([]);
+      setRbacOptions(rbacs);
+
+      setRoles(roles as any);
+    } catch (error) {
+      setHasPermission(false);
+    } finally {
+      setLoading(false);
+    }
   };
   };
 
 
   const onUpdate = async (data: { isEditing: boolean }) => {
   const onUpdate = async (data: { isEditing: boolean }) => {
@@ -71,7 +87,7 @@ const Roles = () => {
   const handleDelete = async (force?: boolean) => {
   const handleDelete = async (force?: boolean) => {
     for (const role of selectedRole) {
     for (const role of selectedRole) {
       const param: DeleteRoleParams = {
       const param: DeleteRoleParams = {
-        roleName: role.name,
+        roleName: role.roleName,
         force,
         force,
       };
       };
       await UserService.deleteRole(param);
       await UserService.deleteRole(param);
@@ -92,6 +108,7 @@ const Roles = () => {
           params: {
           params: {
             component: (
             component: (
               <UpdateRoleDialog
               <UpdateRoleDialog
+                role={{ roleName: '', privileges: {} }}
                 onUpdate={onUpdate}
                 onUpdate={onUpdate}
                 handleClose={handleCloseDialog}
                 handleClose={handleCloseDialog}
               />
               />
@@ -106,7 +123,7 @@ const Roles = () => {
       type: 'button',
       type: 'button',
       btnVariant: 'text',
       btnVariant: 'text',
       btnColor: 'secondary',
       btnColor: 'secondary',
-      label: userTrans('editRole'),
+      label: btnTrans('edit'),
       onClick: async () => {
       onClick: async () => {
         setDialog({
         setDialog({
           open: true,
           open: true,
@@ -126,11 +143,42 @@ const Roles = () => {
       disabled: () =>
       disabled: () =>
         selectedRole.length === 0 ||
         selectedRole.length === 0 ||
         selectedRole.length > 1 ||
         selectedRole.length > 1 ||
-        selectedRole.findIndex(v => v.name === 'admin') > -1 ||
-        selectedRole.findIndex(v => v.name === 'public') > -1,
+        selectedRole.findIndex(v => v.roleName === 'admin') > -1 ||
+        selectedRole.findIndex(v => v.roleName === 'public') > -1,
+      disabledTooltip: userTrans('disableEditRolePrivilegeTip'),
+    },
+    {
+      type: 'button',
+      btnVariant: 'text',
+      btnColor: 'secondary',
+      label: btnTrans('duplicate'),
+      onClick: async () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <UpdateRoleDialog
+                role={{
+                  ...selectedRole[0],
+                  roleName: selectedRole[0].roleName + '_copy',
+                }}
+                onUpdate={onUpdate}
+                handleClose={handleCloseDialog}
+                sameAs={true}
+              />
+            ),
+          },
+        });
+      },
+      icon: 'copy',
+      disabled: () =>
+        selectedRole.length === 0 ||
+        selectedRole.length > 1 ||
+        selectedRole.findIndex(v => v.roleName === 'admin') > -1 ||
+        selectedRole.findIndex(v => v.roleName === 'public') > -1,
       disabledTooltip: userTrans('disableEditRolePrivilegeTip'),
       disabledTooltip: userTrans('disableEditRolePrivilegeTip'),
     },
     },
-
     {
     {
       type: 'button',
       type: 'button',
       btnVariant: 'text',
       btnVariant: 'text',
@@ -144,7 +192,7 @@ const Roles = () => {
               <DeleteTemplate
               <DeleteTemplate
                 label={btnTrans('drop')}
                 label={btnTrans('drop')}
                 title={dialogTrans('deleteTitle', { type: userTrans('role') })}
                 title={dialogTrans('deleteTitle', { type: userTrans('role') })}
-                text={userTrans('deleteWarning')}
+                text={userTrans('deleteRoleWarning')}
                 handleDelete={handleDelete}
                 handleDelete={handleDelete}
                 forceDelLabel={userTrans('forceDelLabel')}
                 forceDelLabel={userTrans('forceDelLabel')}
               />
               />
@@ -155,47 +203,103 @@ const Roles = () => {
       label: btnTrans('drop'),
       label: btnTrans('drop'),
       disabled: () =>
       disabled: () =>
         selectedRole.length === 0 ||
         selectedRole.length === 0 ||
-        selectedRole.findIndex(v => v.name === 'admin') > -1 ||
-        selectedRole.findIndex(v => v.name === 'public') > -1,
-      disabledTooltip: userTrans('deleteTip'),
+        selectedRole.findIndex(v => v.roleName === 'admin') > -1 ||
+        selectedRole.findIndex(v => v.roleName === 'public') > -1,
+      disabledTooltip: userTrans('deleteRoleTip'),
       icon: 'delete',
       icon: 'delete',
     },
     },
   ];
   ];
 
 
   const colDefinitions: ColDefinitionsType[] = [
   const colDefinitions: ColDefinitionsType[] = [
     {
     {
-      id: 'name',
+      id: 'roleName',
       align: 'left',
       align: 'left',
       disablePadding: false,
       disablePadding: false,
       label: userTrans('role'),
       label: userTrans('role'),
+      sortType: 'string',
     },
     },
 
 
     {
     {
-      id: 'privilegeContent',
+      id: 'privileges',
       align: 'left',
       align: 'left',
       disablePadding: false,
       disablePadding: false,
-      formatter({ privilegeContent }) {
+      formatter({ privileges, roleName }) {
+        const isAdmin = roleName === 'admin';
+
+        // Derive the options arrays as in DBCollectionSelector.
+        const rbacEntries = Object.entries(rbacOptions) as [
+          string,
+          Record<string, string>
+        ][];
+
+        // privileges of the privilege groups
+        const privilegeGroups = rbacEntries.filter(([key]) =>
+          key.endsWith('PrivilegeGroups')
+        );
+
+        const groupPrivileges = new Set(
+          privilegeGroups.reduce(
+            (acc, [_, group]) => acc.concat(Object.values(group)),
+            [] as string[]
+          )
+        );
+
+        let groupCount = 0;
+        let privilegeCount = 0;
+
+        Object.values(privileges as DBCollectionsPrivileges).forEach(
+          dbPrivileges => {
+            Object.values(dbPrivileges.collections).forEach(
+              collectionPrivileges => {
+                Object.keys(collectionPrivileges).forEach(privilege => {
+                  if (groupPrivileges.has(privilege)) {
+                    groupCount++;
+                  } else {
+                    privilegeCount++;
+                  }
+                });
+              }
+            );
+          }
+        );
+
         return (
         return (
-          <>
-            {privilegeContent.entities.map((e: any) => {
-              return (
-                <Chip
-                  key={`${e.object.name}-${e.grantor.privilege.name}`}
-                  className={classes.chip}
-                  size="small"
-                  label={e.grantor.privilege.name}
-                  variant="outlined"
-                />
-              );
-            })}
-          </>
+          <div>
+            {
+              <>
+                <div style={{ marginBottom: 2 }}>
+                  <Chip
+                    label={`${userTrans('Group')} (${
+                      isAdmin ? '*' : groupCount
+                    })`}
+                    size="small"
+                    style={{ marginRight: 2 }}
+                  />
+                </div>
+                <div style={{ marginBottom: 2 }}>
+                  <Chip
+                    label={`${userTrans('privileges')} (${
+                      isAdmin ? '*' : privilegeCount
+                    })`}
+                    size="small"
+                    style={{ marginRight: 2 }}
+                  />
+                </div>
+              </>
+            }
+          </div>
         );
         );
       },
       },
       label: userTrans('privileges'),
       label: userTrans('privileges'),
+      getStyle: () => {
+        return {
+          width: '80%',
+        };
+      },
     },
     },
   ];
   ];
 
 
-  const handleSelectChange = (value: RoleData[]) => {
+  const handleSelectChange = (value: any[]) => {
     setSelectedRole(value);
     setSelectedRole(value);
   };
   };
 
 
@@ -220,19 +324,20 @@ const Roles = () => {
   };
   };
 
 
   return (
   return (
-    <div className={classes.wrapper}>
+    <Wrapper className={classes.wrapper} hasPermission={hasPermission}>
       <AttuGrid
       <AttuGrid
         toolbarConfigs={toolbarConfigs}
         toolbarConfigs={toolbarConfigs}
         colDefinitions={colDefinitions}
         colDefinitions={colDefinitions}
         rows={result}
         rows={result}
         rowCount={total}
         rowCount={total}
-        primaryKey="name"
+        primaryKey="roleName"
         showPagination={true}
         showPagination={true}
         selected={selectedRole}
         selected={selectedRole}
         setSelected={handleSelectChange}
         setSelected={handleSelectChange}
         page={currentPage}
         page={currentPage}
         onPageChange={handlePageChange}
         onPageChange={handlePageChange}
         rowsPerPage={pageSize}
         rowsPerPage={pageSize}
+        rowHeight={69}
         setRowsPerPage={handlePageSize}
         setRowsPerPage={handlePageSize}
         isLoading={loading}
         isLoading={loading}
         order={order}
         order={order}
@@ -240,7 +345,7 @@ const Roles = () => {
         handleSort={handleGridSort}
         handleSort={handleGridSort}
         labelDisplayedRows={getLabelDisplayedRows(userTrans('roles'))}
         labelDisplayedRows={getLabelDisplayedRows(userTrans('roles'))}
       />
       />
-    </div>
+    </Wrapper>
   );
   );
 };
 };
 
 

+ 33 - 37
client/src/pages/user/Types.ts

@@ -1,10 +1,8 @@
 import { Option as RoleOption } from '@/components/customSelector/Types';
 import { Option as RoleOption } from '@/components/customSelector/Types';
-
-export interface UserData {
-  name: string;
-  roleName?: string;
-  roles: string[];
-}
+import type {
+  DBCollectionsPrivileges,
+  RBACOptions,
+} from '@server/types/users.type';
 
 
 export interface CreateUserParams {
 export interface CreateUserParams {
   username: string;
   username: string;
@@ -46,16 +44,9 @@ export interface DeleteUserParams {
   username: string;
   username: string;
 }
 }
 
 
-export interface Privilege {
-  roleName: string;
-  object: string;
-  objectName: string;
-  privilegeName: string;
-}
-
 export interface CreateRoleParams {
 export interface CreateRoleParams {
   roleName: string;
   roleName: string;
-  privileges: Privilege[];
+  privileges: DBCollectionsPrivileges;
 }
 }
 
 
 export interface CreatePrivilegeGroupParams {
 export interface CreatePrivilegeGroupParams {
@@ -63,15 +54,16 @@ export interface CreatePrivilegeGroupParams {
   privileges: string[];
   privileges: string[];
 }
 }
 
 
-export interface RoleData {
-  name: string;
-  privileges: Privilege[];
-}
+export type RoleData = {
+  roleName: string;
+  privileges: DBCollectionsPrivileges;
+};
 
 
 export interface CreateRoleProps {
 export interface CreateRoleProps {
   onUpdate: (data: { data: CreateRoleParams; isEditing: boolean }) => void;
   onUpdate: (data: { data: CreateRoleParams; isEditing: boolean }) => void;
   handleClose: () => void;
   handleClose: () => void;
-  role?: RoleData;
+  role: RoleData;
+  sameAs?: boolean;
 }
 }
 
 
 export interface DeleteRoleParams {
 export interface DeleteRoleParams {
@@ -86,29 +78,33 @@ export interface AssignRoleParams {
 
 
 export interface UnassignRoleParams extends AssignRoleParams {}
 export interface UnassignRoleParams extends AssignRoleParams {}
 
 
-export type RBACObject = 'Global' | 'Collection' | 'User';
+export type CollectionOption = {
+  name: string;
+  value: string;
+};
 
 
-export interface PrivilegeOptionsProps {
-  options: string[];
-  selection: Privilege[];
-  onChange: (selection: Privilege[]) => void;
-  roleName: string;
-  object: RBACObject;
-  objectName?: string;
-  title: string;
+export type DBOption = {
+  name: string;
+  value: string;
+};
+
+export interface DBCollectionsSelectorProps {
+  selected: DBCollectionsPrivileges; // Current selected DBs and their collections with privileges
+  setSelected: (
+    value:
+      | DBCollectionsPrivileges
+      | ((prev: DBCollectionsPrivileges) => DBCollectionsPrivileges)
+  ) => void;
+  // Callback to update selected state
+  options: {
+    rbacOptions: RBACOptions; // Available RBAC options (privileges)
+    dbOptions: DBOption[]; // Available databases
+  };
 }
 }
 
 
 export interface PrivilegeGrpOptionsProps {
 export interface PrivilegeGrpOptionsProps {
   options: string[];
   options: string[];
   selection: string[];
   selection: string[];
-  onChange: (selection: string[]) => void;
+  onChange: (data: string[]) => void;
   group_name: string;
   group_name: string;
 }
 }
-
-export type RBACOptions = {
-  GlobalPrivileges: Record<string, unknown>;
-  CollectionPrivileges: Record<string, unknown>;
-  RbacObjects: Record<string, unknown>;
-  UserPrivileges: Record<string, unknown>;
-  Privileges: Record<string, unknown>;
-};

+ 70 - 64
client/src/pages/user/User.tsx

@@ -8,10 +8,10 @@ import {
   CreateUserParams,
   CreateUserParams,
   DeleteUserParams,
   DeleteUserParams,
   UpdateUserParams,
   UpdateUserParams,
-  UserData,
   UpdateUserRoleParams,
   UpdateUserRoleParams,
 } from './Types';
 } from './Types';
 import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
 import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
+import Wrapper from '@/components/layout/Wrapper';
 import { rootContext } from '@/context';
 import { rootContext } from '@/context';
 import { useNavigationHook, usePaginationHook } from '@/hooks';
 import { useNavigationHook, usePaginationHook } from '@/hooks';
 import CreateUser from './dialogs/CreateUserDialog';
 import CreateUser from './dialogs/CreateUserDialog';
@@ -19,6 +19,7 @@ import UpdateUserRole from './dialogs/UpdateUserRole';
 import UpdateUser from './dialogs/UpdateUserPassDialog';
 import UpdateUser from './dialogs/UpdateUserPassDialog';
 import { ALL_ROUTER_TYPES } from '@/router/consts';
 import { ALL_ROUTER_TYPES } from '@/router/consts';
 import { makeStyles } from '@mui/styles';
 import { makeStyles } from '@mui/styles';
+import type { UserWithRoles } from '@server/types';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
   wrapper: {
@@ -28,36 +29,34 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 
 const Users = () => {
 const Users = () => {
   useNavigationHook(ALL_ROUTER_TYPES.USER);
   useNavigationHook(ALL_ROUTER_TYPES.USER);
+  // styles
   const classes = useStyles();
   const classes = useStyles();
 
 
-  const [users, setUsers] = useState<UserData[]>([]);
-  const [selectedUser, setSelectedUser] = useState<UserData[]>([]);
+  // ui states
+  const [users, setUsers] = useState<UserWithRoles[]>([]);
+  const [selectedUser, setSelectedUser] = useState<UserWithRoles[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [hasPermission, setHasPermission] = useState(true);
+  // context
   const { setDialog, handleCloseDialog, openSnackBar } =
   const { setDialog, handleCloseDialog, openSnackBar } =
     useContext(rootContext);
     useContext(rootContext);
+  // i18n
   const { t: successTrans } = useTranslation('success');
   const { t: successTrans } = useTranslation('success');
   const { t: userTrans } = useTranslation('user');
   const { t: userTrans } = useTranslation('user');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: dialogTrans } = useTranslation('dialog');
 
 
   const fetchUsers = async () => {
   const fetchUsers = async () => {
-    const res = await UserService.getUsers();
-    const roles = await UserService.getRoles();
+    setLoading(true);
+    try {
+      const res = await UserService.getUsers();
 
 
-    setUsers(
-      res.usernames.map((v: string) => {
-        const name = v;
-        const rolesByName = roles.results.filter(r =>
-          r.users.map((u: any) => u.name).includes(name)
-        );
-        const originRoles =
-          v === 'root' ? ['admin'] : rolesByName.map(r => r.role.name);
-        return {
-          name: v,
-          role: originRoles.join(' , '),
-          roles: originRoles,
-        };
-      })
-    );
+      setUsers(res);
+    } catch (error) {
+      setHasPermission(false);
+    } finally {
+      setLoading(false);
+    }
   };
   };
 
 
   const {
   const {
@@ -103,7 +102,7 @@ const Users = () => {
   const handleDelete = async () => {
   const handleDelete = async () => {
     for (const user of selectedUser) {
     for (const user of selectedUser) {
       const param: DeleteUserParams = {
       const param: DeleteUserParams = {
-        username: user.name,
+        username: user.username,
       };
       };
       await UserService.deleteUser(param);
       await UserService.deleteUser(param);
     }
     }
@@ -126,8 +125,8 @@ const Users = () => {
               <CreateUser
               <CreateUser
                 handleCreate={handleCreate}
                 handleCreate={handleCreate}
                 handleClose={handleCloseDialog}
                 handleClose={handleCloseDialog}
-                roleOptions={roles.results.map((r: any) => {
-                  return { label: r.role.name, value: r.role.name };
+                roleOptions={roles.map(r => {
+                  return { label: r.roleName, value: r.roleName };
                 })}
                 })}
               />
               />
             ),
             ),
@@ -137,6 +136,34 @@ const Users = () => {
       icon: 'add',
       icon: 'add',
     },
     },
 
 
+    {
+      type: 'button',
+      btnVariant: 'text',
+      btnColor: 'secondary',
+      label: userTrans('editPassword'),
+      onClick: async () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <UpdateUser
+                username={selectedUser[0]!.username}
+                handleUpdate={handleUpdate}
+                handleClose={handleCloseDialog}
+              />
+            ),
+          },
+        });
+      },
+      icon: 'edit',
+      disabled: () =>
+        selectedUser.length === 0 ||
+        selectedUser.length > 1 ||
+        selectedUser.findIndex(v => v.username === 'root') > -1,
+      disabledTooltip: userTrans('deleteEditRoleTip'),
+    },
+
     {
     {
       type: 'button',
       type: 'button',
       btnVariant: 'text',
       btnVariant: 'text',
@@ -149,11 +176,12 @@ const Users = () => {
           params: {
           params: {
             component: (
             component: (
               <UpdateUserRole
               <UpdateUserRole
-                username={selectedUser[0]!.name}
+                username={selectedUser[0]!.username}
                 onUpdate={onUpdate}
                 onUpdate={onUpdate}
                 handleClose={handleCloseDialog}
                 handleClose={handleCloseDialog}
                 roles={
                 roles={
-                  users.filter(u => u.name === selectedUser[0].name)[0].roles
+                  users.filter(u => u.username === selectedUser[0].username)[0]
+                    .roles
                 }
                 }
               />
               />
             ),
             ),
@@ -164,7 +192,7 @@ const Users = () => {
       disabled: () =>
       disabled: () =>
         selectedUser.length === 0 ||
         selectedUser.length === 0 ||
         selectedUser.length > 1 ||
         selectedUser.length > 1 ||
-        selectedUser.findIndex(v => v.name === 'root') > -1,
+        selectedUser.findIndex(v => v.username === 'root') > -1,
       disabledTooltip: userTrans('deleteEditRoleTip'),
       disabledTooltip: userTrans('deleteEditRoleTip'),
     },
     },
 
 
@@ -191,7 +219,7 @@ const Users = () => {
       label: btnTrans('drop'),
       label: btnTrans('drop'),
       disabled: () =>
       disabled: () =>
         selectedUser.length === 0 ||
         selectedUser.length === 0 ||
-        selectedUser.findIndex(v => v.name === 'root') > -1,
+        selectedUser.findIndex(v => v.username === 'root') > -1,
       disabledTooltip: userTrans('deleteTip'),
       disabledTooltip: userTrans('deleteTip'),
       icon: 'delete',
       icon: 'delete',
     },
     },
@@ -199,50 +227,28 @@ const Users = () => {
 
 
   const colDefinitions: ColDefinitionsType[] = [
   const colDefinitions: ColDefinitionsType[] = [
     {
     {
-      id: 'name',
+      id: 'username',
       align: 'left',
       align: 'left',
       sortType: 'string',
       sortType: 'string',
       disablePadding: false,
       disablePadding: false,
       label: userTrans('user'),
       label: userTrans('user'),
     },
     },
     {
     {
-      id: 'role',
+      id: 'roles',
       align: 'left',
       align: 'left',
-      sortType: 'string',
-      disablePadding: false,
+      notSort: true,
+      disablePadding: true,
       label: userTrans('role'),
       label: userTrans('role'),
-    },
-    {
-      id: 'action',
-      disablePadding: false,
-      label: 'Action',
-      showActionCell: true,
-      sortBy: 'action',
-      actionBarConfigs: [
-        {
-          onClick: (e: React.MouseEvent, row: UserData) => {
-            setDialog({
-              open: true,
-              type: 'custom',
-              params: {
-                component: (
-                  <UpdateUser
-                    username={row.name}
-                    handleUpdate={handleUpdate}
-                    handleClose={handleCloseDialog}
-                  />
-                ),
-              },
-            });
-          },
-          linkButton: true,
-          text: 'Update password',
-        },
-      ],
+      formatter(rowData, cellData) {
+        return rowData.username === 'root' ? 'admin' : cellData.join(', ');
+      },
+      getStyle: () => {
+        return { width: '80%' };
+      },
     },
     },
   ];
   ];
 
 
-  const handleSelectChange = (value: UserData[]) => {
+  const handleSelectChange = (value: UserWithRoles[]) => {
     setSelectedUser(value);
     setSelectedUser(value);
   };
   };
 
 
@@ -255,13 +261,13 @@ const Users = () => {
   };
   };
 
 
   return (
   return (
-    <div className={classes.wrapper}>
+    <Wrapper className={classes.wrapper} hasPermission={hasPermission}>
       <AttuGrid
       <AttuGrid
         toolbarConfigs={toolbarConfigs}
         toolbarConfigs={toolbarConfigs}
         colDefinitions={colDefinitions}
         colDefinitions={colDefinitions}
         rows={result}
         rows={result}
         rowCount={total}
         rowCount={total}
-        primaryKey="name"
+        primaryKey="username"
         showPagination={true}
         showPagination={true}
         selected={selectedUser}
         selected={selectedUser}
         setSelected={handleSelectChange}
         setSelected={handleSelectChange}
@@ -269,12 +275,12 @@ const Users = () => {
         onPageChange={handlePageChange}
         onPageChange={handlePageChange}
         rowsPerPage={pageSize}
         rowsPerPage={pageSize}
         setRowsPerPage={handlePageSize}
         setRowsPerPage={handlePageSize}
-        // isLoading={loading}
+        isLoading={loading}
         order={order}
         order={order}
         orderBy={orderBy}
         orderBy={orderBy}
         handleSort={handleGridSort}
         handleSort={handleGridSort}
       />
       />
-    </div>
+    </Wrapper>
   );
   );
 };
 };
 
 

+ 545 - 0
client/src/pages/user/dialogs/DBCollectionSelector.tsx

@@ -0,0 +1,545 @@
+import { useState, useCallback, useEffect } from 'react';
+import {
+  TextField,
+  Typography,
+  FormControlLabel,
+  Checkbox,
+  Tabs,
+  Tab,
+  Radio,
+} from '@mui/material';
+import Autocomplete from '@mui/material/Autocomplete';
+import { CollectionService } from '@/http';
+import { useTranslation } from 'react-i18next';
+import { useDBCollectionSelectorStyle } from './styles';
+import type {
+  DBOption,
+  CollectionOption,
+  DBCollectionsSelectorProps,
+} from '../Types';
+import type { DBCollectionsPrivileges, RBACOptions } from '@server/types';
+
+export default function DBCollectionsSelector(
+  props: DBCollectionsSelectorProps
+) {
+  // Props
+  const { selected, setSelected, options } = props;
+  const { rbacOptions, dbOptions } = options;
+  // Styles
+  const classes = useDBCollectionSelectorStyle();
+  // i18n
+  const { t: searchTrans } = useTranslation('search');
+  const { t: userTrans } = useTranslation('user');
+
+  // UI states
+  const [selectedDB, setSelectedDB] = useState<DBOption | null>(null);
+  const [selectedCollection, setSelectedCollection] = useState<string>('*');
+  const [collectionOptions, setCollectionOptions] = useState<
+    CollectionOption[]
+  >([]);
+  const [loading, setLoading] = useState(false);
+  const [tabValue, setTabValue] = useState(0); // Tab index
+  const [privilegeOptionType, setPrivilegeOptionType] = useState<
+    'group' | 'custom'
+  >('group'); // Type of privilege options
+
+  // Fetch collections when selected DB changes
+  const fetchCollections = useCallback(async (dbName: string) => {
+    // const
+    const ALL_COLLECTIONS = { name: '*', value: '*' };
+    setLoading(true);
+    try {
+      let options: CollectionOption[] = [];
+      if (dbName === '*') {
+        options = [ALL_COLLECTIONS];
+      } else {
+        const res = await CollectionService.getCollectionsNames({
+          db_name: dbName,
+        });
+        options = res.map(c => ({ name: c, value: c }));
+        options.unshift(ALL_COLLECTIONS);
+      }
+      setCollectionOptions(options);
+    } catch (err) {
+      console.error(err);
+    } finally {
+      setLoading(false);
+    }
+  }, []);
+
+  // Initialize selectedDB and fetch collections when dbOptions changes
+  useEffect(() => {
+    if (dbOptions.length > 0 && selectedDB === null) {
+      const initialDB = dbOptions[0];
+      setSelectedDB(initialDB);
+      fetchCollections(initialDB.value);
+    }
+  }, [dbOptions, selectedDB, fetchCollections]);
+
+  // Handle DB selection
+  const handleDBChange = (db: DBOption) => {
+    setSelectedDB(db);
+    setSelected(prevSelected => {
+      const newSelected = { ...prevSelected } as DBCollectionsPrivileges;
+      if (!newSelected[db.value]) {
+        newSelected[db.value] = { collections: {} };
+      }
+      setSelectedCollection('*');
+      return newSelected;
+    });
+    fetchCollections(db.value);
+  };
+
+  // Handle collection selection
+  const handleCollectionChange = (collection: CollectionOption | null) => {
+    if (collection) {
+      setSelectedCollection(collection.value);
+    }
+  };
+
+  // Handle privilege change for a collection
+  const handlePrivilegeChange = (
+    collectionValue: string,
+    privilegeName: string,
+    checked: boolean,
+    dbValue: string
+  ) => {
+    const selectedDBValue = dbValue;
+    if (!selectedDBValue) return;
+
+    const newSelected = { ...selected };
+    if (!newSelected[selectedDBValue]) {
+      newSelected[selectedDBValue] = { collections: {} };
+    }
+    if (!newSelected[selectedDBValue].collections[collectionValue]) {
+      newSelected[selectedDBValue].collections[collectionValue] = {};
+    }
+    newSelected[selectedDBValue].collections[collectionValue][privilegeName] =
+      checked;
+    setSelected(newSelected);
+  };
+
+  // Handle select all privileges in a category
+  const handleSelectAll = (
+    category: keyof RBACOptions,
+    collectionValue: string,
+    checked: boolean,
+    dbValue: string
+  ) => {
+    const selectedDBValue = dbValue;
+    if (!selectedDBValue) return;
+
+    const newSelected = { ...selected };
+    if (!newSelected[selectedDBValue]) {
+      newSelected[selectedDBValue] = { collections: {} };
+    }
+    if (!newSelected[selectedDBValue].collections[collectionValue]) {
+      newSelected[selectedDBValue].collections[collectionValue] = {};
+    }
+    Object.keys(rbacOptions[category]).forEach(privilegeName => {
+      newSelected[selectedDBValue].collections[collectionValue][privilegeName] =
+        checked;
+    });
+    setSelected(newSelected);
+  };
+
+  // Check if all privileges in a category are selected
+  const isCategoryAllSelected = (
+    category: keyof RBACOptions,
+    collectionValue: string,
+    dbValue: string
+  ) => {
+    const selectedDBValue = dbValue || selectedDB?.value;
+    if (!selectedDBValue) return false;
+
+    const categoryPrivileges = rbacOptions[category];
+    return Object.keys(categoryPrivileges).every(
+      privilegeName =>
+        selected[selectedDBValue] &&
+        selected[selectedDBValue].collections &&
+        selected[selectedDBValue].collections[collectionValue]?.[privilegeName]
+    );
+  };
+
+  // Check if some privileges in a category are selected
+  const isCategorySomeSelected = (
+    category: keyof RBACOptions,
+    collectionValue: string,
+    dbValue: string
+  ) => {
+    const selectedDBValue = dbValue;
+    if (!selectedDBValue) return false;
+
+    const categoryPrivileges = rbacOptions[category];
+    const someSelected = Object.keys(categoryPrivileges).some(
+      privilegeName =>
+        selected[selectedDBValue] &&
+        selected[selectedDBValue].collections &&
+        selected[selectedDBValue].collections[collectionValue]?.[privilegeName]
+    );
+    const allSelected = isCategoryAllSelected(
+      category,
+      collectionValue,
+      dbValue
+    );
+    return someSelected && !allSelected;
+  };
+
+  // Calculate the number of selected privileges for a collection
+  const getSelectedPrivilegesCount = (
+    collectionValue: string,
+    privileges: [string, Record<string, string>][]
+  ): number => {
+    const selectedDBValue = selectedDB?.value;
+    if (!selectedDBValue) return 0;
+
+    const collectionPrivileges =
+      selected[selectedDBValue]?.collections?.[collectionValue] || {};
+
+    // Count the number of privileges that are selected for the collection
+    const selectedCount = privileges.reduce(
+      (total, [_, categoryPrivileges]) => {
+        return (
+          total +
+          Object.keys(categoryPrivileges).reduce((total, privilegeName) => {
+            return total + (collectionPrivileges[privilegeName] ? 1 : 0);
+          }, 0)
+        );
+      },
+      0
+    );
+
+    return selectedCount;
+  };
+
+  // Calculate the total number of privileges for a collection
+  const getTotalPrivilegesCount = (
+    privileges: [string, Record<string, string>][]
+  ) => {
+    const total = privileges.reduce((total, [_, categoryPrivileges]) => {
+      return total + Object.keys(categoryPrivileges).length;
+    }, 0);
+
+    return total;
+  };
+
+  // extract privileges options
+  const globalPrivileges = [
+    'DatabasePrivileges',
+    'ResourceManagementPrivileges',
+    'RBACPrivileges',
+  ];
+
+  const rbacEntries = Object.entries(rbacOptions) as [
+    keyof RBACOptions,
+    Record<string, string>
+  ][];
+  const databasePrivilegeOptions = rbacEntries.filter(([category]) => {
+    return (
+      category === 'DatabasePrivileges' || category == 'DatabasePrivilegeGroups'
+    );
+  });
+  const collectionPrivilegeOptions = rbacEntries.filter(([category]) => {
+    return (
+      !globalPrivileges.includes(category) &&
+      category !== 'ClusterPrivilegeGroups' &&
+      category !== 'DatabasePrivilegeGroups'
+    );
+  });
+  const clusterPrivileges = rbacEntries.filter(([category]) => {
+    return (
+      category === 'ResourceManagementPrivileges' ||
+      category === 'RBACPrivileges' ||
+      category === 'ClusterPrivilegeGroups' ||
+      category === 'CustomPrivilegeGroups'
+    );
+  });
+
+  const collectionTypeOptions = collectionPrivilegeOptions.filter(c => {
+    return privilegeOptionType === 'group'
+      ? c[0].includes('Groups')
+      : !c[0].includes('Groups');
+  });
+
+  return (
+    <div className={classes.root}>
+      {/* Privilege type toggle */}
+      <div className={classes.toggle}>
+        <label className="toggle-label">
+          <Radio
+            checked={privilegeOptionType === 'group'}
+            onChange={() => setPrivilegeOptionType('group')}
+            value="group"
+            name="group"
+            size="small"
+            inputProps={{ 'aria-label': 'group' }}
+          />
+          {userTrans('privilegeGroups')}
+        </label>
+        <label className="toggle-label">
+          <Radio
+            checked={privilegeOptionType === 'custom'}
+            onChange={() => setPrivilegeOptionType('custom')}
+            value="custom"
+            name="custom"
+            size="small"
+            inputProps={{ 'aria-label': 'custom' }}
+          />
+          {userTrans('privileges')}
+        </label>
+      </div>
+      {/* Tabs for cluster, Database, Collection */}
+      <Tabs
+        value={tabValue}
+        onChange={(event, newValue) => setTabValue(newValue)}
+        aria-label="tabs"
+      >
+        <Tab label={userTrans('collection')} sx={{ textTransform: 'none' }} />
+        <Tab label={userTrans('database')} sx={{ textTransform: 'none' }} />
+        <Tab label={userTrans('cluster')} sx={{ textTransform: 'none' }} />
+      </Tabs>
+
+      {tabValue === 2 && (
+        <PrivilegeSelector
+          privilegeOptions={clusterPrivileges}
+          selected={selected}
+          selectedDB={{ name: userTrans('allDatabases'), value: '*' }}
+          selectedCollection={'*'}
+          handlePrivilegeChange={handlePrivilegeChange}
+          isCategoryAllSelected={isCategoryAllSelected}
+          isCategorySomeSelected={isCategorySomeSelected}
+          handleSelectAll={handleSelectAll}
+          privilegeOptionType={privilegeOptionType}
+        />
+      )}
+
+      {tabValue === 1 && (
+        <PrivilegeSelector
+          privilegeOptions={databasePrivilegeOptions}
+          selected={selected}
+          selectedDB={{ name: userTrans('allDatabases'), value: '*' }}
+          selectedCollection={'*'}
+          handlePrivilegeChange={handlePrivilegeChange}
+          isCategoryAllSelected={isCategoryAllSelected}
+          isCategorySomeSelected={isCategorySomeSelected}
+          handleSelectAll={handleSelectAll}
+          privilegeOptionType={privilegeOptionType}
+        />
+      )}
+
+      {tabValue === 0 && (
+        <div>
+          <div className={classes.dbCollections}>
+            <Autocomplete
+              className={classes.selectorDB}
+              options={dbOptions}
+              loading={loading}
+              value={selectedDB || null}
+              onChange={(_, value) => {
+                if (!value) return;
+                handleDBChange(value);
+              }}
+              getOptionLabel={option => option.name}
+              isOptionEqualToValue={(option, value) =>
+                option.value === value.value
+              }
+              renderInput={params => (
+                <TextField
+                  {...params}
+                  label={userTrans('databases')}
+                  variant="filled"
+                />
+              )}
+              noOptionsText={
+                loading ? searchTrans('loading') : searchTrans('noOptions')
+              }
+            />
+            <Autocomplete
+              className={classes.selectorCollection}
+              options={collectionOptions}
+              loading={loading}
+              value={
+                collectionOptions.find(
+                  option => option.value === selectedCollection
+                ) || null
+              }
+              onChange={(_, value) => {
+                if (!value) return;
+                handleCollectionChange(value);
+              }}
+              getOptionLabel={option => {
+                const selectedCount = getSelectedPrivilegesCount(
+                  option.value,
+                  collectionTypeOptions
+                );
+                const totalCount = getTotalPrivilegesCount(
+                  collectionTypeOptions
+                );
+                return `${option.name} (${selectedCount}/${totalCount})`;
+              }}
+              isOptionEqualToValue={(option, value) =>
+                option.value === value.value
+              }
+              renderInput={params => {
+                return (
+                  <TextField
+                    {...params}
+                    label={userTrans('collections')}
+                    variant="filled"
+                  />
+                );
+              }}
+              noOptionsText={
+                loading ? searchTrans('loading') : searchTrans('noOptions')
+              }
+            />
+          </div>
+
+          <PrivilegeSelector
+            privilegeOptions={collectionPrivilegeOptions}
+            selected={selected}
+            selectedDB={selectedDB}
+            selectedCollection={selectedCollection}
+            handlePrivilegeChange={handlePrivilegeChange}
+            isCategoryAllSelected={isCategoryAllSelected}
+            isCategorySomeSelected={isCategorySomeSelected}
+            handleSelectAll={handleSelectAll}
+            privilegeOptionType={privilegeOptionType}
+          />
+        </div>
+      )}
+    </div>
+  );
+}
+
+// PriviligeSelector
+const PrivilegeSelector = (props: {
+  privilegeOptions: [keyof RBACOptions, Record<string, string>][];
+  selected: DBCollectionsPrivileges;
+  selectedDB: DBOption | null;
+  selectedCollection: string;
+  privilegeOptionType: 'group' | 'custom';
+  handlePrivilegeChange: (
+    collectionValue: string,
+    privilegeName: string,
+    checked: boolean,
+    dbValue: string
+  ) => void;
+  isCategoryAllSelected: (
+    category: keyof RBACOptions,
+    collectionValue: string,
+    dbValue: string
+  ) => boolean;
+  isCategorySomeSelected: (
+    category: keyof RBACOptions,
+    collectionValue: string,
+    dbValue: string
+  ) => boolean;
+  handleSelectAll: (
+    category: keyof RBACOptions,
+    collectionValue: string,
+    checked: boolean,
+    dbValue: string
+  ) => void;
+}) => {
+  // props
+  const {
+    selected,
+    selectedDB,
+    selectedCollection,
+    privilegeOptions,
+    handlePrivilegeChange,
+    handleSelectAll,
+    isCategoryAllSelected,
+    isCategorySomeSelected,
+    privilegeOptionType,
+  } = props;
+
+  // style
+  const classes = useDBCollectionSelectorStyle();
+
+  // i18n
+  const { t: userTrans } = useTranslation('user');
+
+  return (
+    <div className={classes.privileges}>
+      {selectedDB && selectedCollection && (
+        <div>
+          {privilegeOptions
+            .filter(v => {
+              if (privilegeOptionType === 'group') {
+                return v[0].includes('Groups');
+              } else {
+                return !v[0].includes('Groups');
+              }
+            })
+            .map(([category, categoryPrivileges]) => (
+              <div key={category}>
+                <div className={classes.categoryHeader}>
+                  <FormControlLabel
+                    control={
+                      <Checkbox
+                        checked={isCategoryAllSelected(
+                          category,
+                          selectedCollection,
+                          selectedDB.value
+                        )}
+                        indeterminate={isCategorySomeSelected(
+                          category,
+                          selectedCollection,
+                          selectedDB.value
+                        )}
+                        onChange={e =>
+                          handleSelectAll(
+                            category,
+                            selectedCollection,
+                            e.target.checked,
+                            selectedDB.value
+                          )
+                        }
+                        size="small"
+                        className={classes.selectAllCheckbox}
+                        title={userTrans('selectAll')}
+                      />
+                    }
+                    label={userTrans(category)}
+                  />
+                </div>
+                <div className={classes.categoryBody}>
+                  {Object.entries(categoryPrivileges).map(([privilegeName]) => (
+                    <FormControlLabel
+                      className={classes.privilegeBody}
+                      key={privilegeName}
+                      control={
+                        <Checkbox
+                          className={classes.checkbox}
+                          checked={
+                            (selected[selectedDB.value] &&
+                              selected[selectedDB.value].collections &&
+                              selected[selectedDB.value].collections[
+                                selectedCollection
+                              ]?.[privilegeName]) ||
+                            false
+                          }
+                          size="small"
+                          onChange={e =>
+                            handlePrivilegeChange(
+                              selectedCollection,
+                              privilegeName,
+                              e.target.checked,
+                              selectedDB.value
+                            )
+                          }
+                        />
+                      }
+                      label={privilegeName}
+                    />
+                  ))}
+                </div>
+              </div>
+            ))}
+        </div>
+      )}
+    </div>
+  );
+};

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

@@ -25,6 +25,7 @@ const PrivilegeGroupOptions: FC<PrivilegeGrpOptionsProps> = ({
   options,
   options,
   selection,
   selection,
   onChange,
   onChange,
+  group_name,
 }) => {
 }) => {
   const classes = useStyles();
   const classes = useStyles();
 
 

+ 0 - 82
client/src/pages/user/dialogs/PrivilegeOptions.tsx

@@ -1,82 +0,0 @@
-import {
-  Theme,
-  Typography,
-  Checkbox,
-  FormGroup,
-  FormControlLabel,
-} from '@mui/material';
-import { FC } from 'react';
-import { makeStyles } from '@mui/styles';
-import type { Privilege, PrivilegeOptionsProps } from '../Types';
-
-const useStyles = makeStyles((theme: Theme) => ({
-  checkBox: {
-    width: theme.spacing(24),
-  },
-  formGrp: {
-    marginBottom: theme.spacing(2),
-  },
-  subTitle: {
-    marginBottom: theme.spacing(0.5),
-  },
-}));
-
-const PrivilegeOptions: FC<PrivilegeOptionsProps> = ({
-  options,
-  selection,
-  onChange,
-  title,
-  roleName,
-  object,
-  objectName = '*',
-}) => {
-  const classes = useStyles();
-
-  return (
-    <>
-      <Typography variant="h6" component="h6" className={classes.subTitle}>
-        {title}
-      </Typography>
-      <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: Privilege) => n.privilegeName !== r
-                    );
-                  } else {
-                    newSelection.push({
-                      privilegeName: r,
-                      object: object,
-                      objectName: objectName,
-                      roleName: roleName,
-                    });
-                  }
-                  onChange(newSelection);
-                }}
-              />
-            }
-            key={r}
-            label={r}
-            value={r}
-            checked={
-              selection.filter((s: Privilege) => s.privilegeName === r).length >
-              0
-            }
-            className={classes.checkBox}
-          />
-        ))}
-      </FormGroup>
-    </>
-  );
-};
-
-export default PrivilegeOptions;

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

@@ -19,7 +19,6 @@ import { CreatePrivilegeGroupParams } from '../Types';
 import PrivilegeGroupOptions from './PrivilegeGroupOptions';
 import PrivilegeGroupOptions from './PrivilegeGroupOptions';
 import { makeStyles } from '@mui/styles';
 import { makeStyles } from '@mui/styles';
 import { PrivilegeGroup } from '@server/types';
 import { PrivilegeGroup } from '@server/types';
-import { Opacity } from '@mui/icons-material';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   input: {
   input: {

+ 107 - 106
client/src/pages/user/dialogs/UpdateRoleDialog.tsx

@@ -1,90 +1,141 @@
-import { Theme, Typography } from '@mui/material';
+import { Theme } from '@mui/material';
 import { FC, useMemo, useState, useEffect } from 'react';
 import { FC, useMemo, useState, useEffect } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import DialogTemplate from '@/components/customDialog/DialogTemplate';
 import DialogTemplate from '@/components/customDialog/DialogTemplate';
 import CustomInput from '@/components/customInput/CustomInput';
 import CustomInput from '@/components/customInput/CustomInput';
 import { useFormValidation } from '@/hooks';
 import { useFormValidation } from '@/hooks';
 import { formatForm } from '@/utils';
 import { formatForm } from '@/utils';
-import { UserService } from '@/http';
+import { UserService, DatabaseService } from '@/http';
 import { makeStyles } from '@mui/styles';
 import { makeStyles } from '@mui/styles';
-import PrivilegeOptions from './PrivilegeOptions';
+import DBCollectionSelector from './DBCollectionSelector';
 import type { ITextfieldConfig } from '@/components/customInput/Types';
 import type { ITextfieldConfig } from '@/components/customInput/Types';
-import type {
-  CreateRoleProps,
-  CreateRoleParams,
-  PrivilegeOptionsProps,
-  RBACOptions,
-} from '../Types';
+import type { CreateRoleProps, CreateRoleParams, DBOption } from '../Types';
+import type { DBCollectionsPrivileges, RBACOptions } from '@server/types';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   input: {
   input: {
     margin: theme.spacing(1, 0, 0.5),
     margin: theme.spacing(1, 0, 0.5),
   },
   },
   dialogWrapper: {
   dialogWrapper: {
-    maxWidth: theme.spacing(88),
-  },
-  checkBox: {
-    width: theme.spacing(24),
-  },
-  formGrp: {
-    marginBottom: theme.spacing(2),
+    width: '66vw',
+    maxWidth: '66vw',
   },
   },
   subTitle: {
   subTitle: {
     marginBottom: theme.spacing(0.5),
     marginBottom: theme.spacing(0.5),
   },
   },
 }));
 }));
 
 
-const UpdateRoleDialog: FC<CreateRoleProps> = ({
-  onUpdate,
-  handleClose,
-  role = { name: '', privileges: [] },
-}) => {
+const DEFAULT_DB_Privileges: DBCollectionsPrivileges = {
+  '*': {
+    collections: {
+      '*': {},
+    },
+  },
+};
+
+const UpdateRoleDialog: FC<CreateRoleProps> = props => {
+  const {
+    role = {
+      roleName: '',
+      privileges: DEFAULT_DB_Privileges,
+    },
+    handleClose,
+    onUpdate,
+    sameAs,
+  } = props;
+  // i18n
   const { t: userTrans } = useTranslation('user');
   const { t: userTrans } = useTranslation('user');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
   const { t: warningTrans } = useTranslation('warning');
   const { t: warningTrans } = useTranslation('warning');
-  const [rbacOptions, setRbacOptions] = useState<RBACOptions>({
-    GlobalPrivileges: {},
-    CollectionPrivileges: {},
-    RbacObjects: {},
-    UserPrivileges: {},
-    Privileges: {},
+
+  // const
+  const ALL_DB = { name: userTrans('allDatabases'), value: '*' };
+
+  // UI states
+  const [options, setOptions] = useState<{
+    rbacOptions: RBACOptions; // Available RBAC options (privileges)
+    dbOptions: DBOption[]; // Available databases
+  }>({
+    rbacOptions: {} as RBACOptions,
+    dbOptions: [],
   });
   });
 
 
-  const fetchRBAC = async () => {
-    const rbacOptions = await UserService.getRBAC();
+  const [selected, setSelected] = useState<DBCollectionsPrivileges>(
+    role.privileges
+  );
 
 
-    setRbacOptions(rbacOptions);
-  };
+  // Form state
+  const [form, setForm] = useState<CreateRoleParams>({
+    roleName: role.roleName,
+    privileges: selected,
+  });
 
 
-  const isEditing = role.name !== '';
+  // update form if selected changes
+  useEffect(() => {
+    setForm(v => ({
+      ...v,
+      privileges: selected,
+    }));
+  }, [selected]);
 
 
   useEffect(() => {
   useEffect(() => {
-    fetchRBAC();
+    const fetchData = async () => {
+      try {
+        const [dbResponse, rbacResponse] = await Promise.all([
+          DatabaseService.listDatabases(),
+          UserService.getRBAC(),
+        ]);
+
+        const dbOptions = dbResponse.map(db => ({
+          name: db.name,
+          value: db.name,
+        }));
+        dbOptions.unshift(ALL_DB);
+
+        setOptions({
+          rbacOptions: rbacResponse,
+          dbOptions,
+        });
+      } catch (err) {
+        console.error('Failed to fetch data:', err);
+      }
+    };
+
+    fetchData();
   }, []);
   }, []);
 
 
-  const [form, setForm] = useState<CreateRoleParams>({
-    roleName: role.name,
-    privileges: JSON.parse(JSON.stringify(role.privileges)),
-  });
+  // Check if editing an existing role
+  const isEditing = role.roleName !== '' && !sameAs;
 
 
+  // Form validation
   const checkedForm = useMemo(() => {
   const checkedForm = useMemo(() => {
     return formatForm(form);
     return formatForm(form);
   }, [form]);
   }, [form]);
   const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
   const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
 
 
-  const classes = useStyles();
-
+  // Handle input change
   const handleInputChange = (key: 'roleName', value: string) => {
   const handleInputChange = (key: 'roleName', value: string) => {
-    setForm(v => {
-      const newFrom = { ...v, [key]: value };
+    setForm(v => ({
+      ...v,
+      [key]: value,
+    }));
+  };
 
 
-      // update roleName
-      newFrom.privileges.forEach(p => (p.roleName = value));
+  // Handle create/update role
+  const handleCreateRole = async () => {
+    try {
+      const res = await UserService.updateRolePrivileges(form);
 
 
-      return newFrom;
-    });
+      onUpdate({ data: form, isEditing });
+    } catch (error) {
+      console.error('Error creating/updating role:', error);
+    }
   };
   };
 
 
+  // styles
+  const classes = useStyles();
+
+  // Input configurations
   const createConfigs: ITextfieldConfig[] = [
   const createConfigs: ITextfieldConfig[] = [
     {
     {
       label: userTrans('role'),
       label: userTrans('role'),
@@ -107,55 +158,14 @@ const UpdateRoleDialog: FC<CreateRoleProps> = ({
     },
     },
   ];
   ];
 
 
-  const handleCreateRole = async () => {
-    if (!isEditing) {
-      await UserService.createRole(form);
-    }
-
-    await UserService.updateRolePrivileges(form);
-
-    onUpdate({ data: form, isEditing: isEditing });
-  };
-
-  const onChange = (newSelection: any) => {
-    setForm(v => {
-      return { ...v, privileges: [...newSelection] };
-    });
-  };
-
-  const optionGroups: PrivilegeOptionsProps[] = [
-    {
-      options: Object.values(rbacOptions.GlobalPrivileges) as string[],
-      object: 'Global',
-      title: userTrans('objectGlobal'),
-      selection: form.privileges,
-      roleName: form.roleName,
-      onChange: onChange,
-    },
-
-    {
-      options: Object.values(rbacOptions.CollectionPrivileges) as string[],
-      title: userTrans('objectCollection'),
-      object: 'Collection',
-      selection: form.privileges,
-      roleName: form.roleName,
-      onChange: onChange,
-    },
-
-    {
-      options: Object.values(rbacOptions.UserPrivileges) as string[],
-      title: userTrans('objectUser'),
-      object: 'User',
-      selection: form.privileges,
-      roleName: form.roleName,
-      onChange: onChange,
-    },
-  ];
-
   return (
   return (
     <DialogTemplate
     <DialogTemplate
       title={userTrans(
       title={userTrans(
-        isEditing ? 'updateRolePrivilegeTitle' : 'createRoleTitle'
+        isEditing
+          ? 'updateRolePrivilegeTitle'
+          : sameAs
+          ? 'dupicateRoleTitle'
+          : 'createRoleTitle'
       )}
       )}
       handleClose={handleClose}
       handleClose={handleClose}
       confirmLabel={btnTrans(isEditing ? 'update' : 'create')}
       confirmLabel={btnTrans(isEditing ? 'update' : 'create')}
@@ -173,21 +183,12 @@ const UpdateRoleDialog: FC<CreateRoleProps> = ({
             key={v.label}
             key={v.label}
           />
           />
         ))}
         ))}
-        <Typography variant="h5" component="h5" className={classes.subTitle}>
-          {userTrans('privileges')}
-        </Typography>
-
-        {optionGroups.map(o => (
-          <PrivilegeOptions
-            key={o.object}
-            title={o.title}
-            object={o.object}
-            options={o.options}
-            selection={o.selection}
-            roleName={o.roleName}
-            onChange={o.onChange}
-          />
-        ))}
+
+        <DBCollectionSelector
+          selected={selected}
+          setSelected={setSelected}
+          options={options}
+        />
       </>
       </>
     </DialogTemplate>
     </DialogTemplate>
   );
   );

+ 2 - 2
client/src/pages/user/dialogs/UpdateUserRole.tsx

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
 import DialogTemplate from '@/components/customDialog/DialogTemplate';
 import DialogTemplate from '@/components/customDialog/DialogTemplate';
 import { UserService } from '@/http';
 import { UserService } from '@/http';
 import { makeStyles } from '@mui/styles';
 import { makeStyles } from '@mui/styles';
-import type { UpdateUserRoleProps, UpdateUserRoleParams } from './Types';
+import type { UpdateUserRoleProps, UpdateUserRoleParams } from '../Types';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   input: {
   input: {
@@ -43,7 +43,7 @@ const UpdateUserRole: FC<UpdateUserRoleProps> = ({
   const fetchAllRoles = async () => {
   const fetchAllRoles = async () => {
     const roles = await UserService.getRoles();
     const roles = await UserService.getRoles();
 
 
-    setRoleOptions(roles.results.map(r => r.role.name));
+    setRoleOptions(roles.map(role => role.roleName));
   };
   };
 
 
   useEffect(() => {
   useEffect(() => {

+ 77 - 0
client/src/pages/user/dialogs/styles.ts

@@ -0,0 +1,77 @@
+import { makeStyles } from '@mui/styles';
+import { Theme } from '@mui/material';
+
+export const useDBCollectionSelectorStyle = makeStyles((theme: Theme) => ({
+  root: {
+    display: 'flex',
+    flexDirection: 'column',
+    gap: theme.spacing(1),
+    backgroundColor: theme.palette.background.paper,
+  },
+  dbCollections: {
+    display: 'flex',
+    flexDirection: 'row',
+    gap: theme.spacing(1),
+  },
+  selectorDB: {
+    flex: 1,
+    marginBottom: theme.spacing(2),
+  },
+  selectorCollection: {
+    flex: 1,
+  },
+  categoryHeader: {
+    display: 'flex',
+    alignItems: 'center',
+    padding: theme.spacing(0.5),
+    backgroundColor: theme.palette.action.hover,
+    borderRadius: theme.shape.borderRadius,
+    marginBottom: 0,
+    '& .MuiTypography-root': {
+      fontSize: 14,
+      fontWeight: 600,
+      color: theme.palette.text.primary,
+    },
+  },
+  categoryBody: {
+    padding: theme.spacing(0.5, 1.5),
+    borderRadius: theme.shape.borderRadius,
+    backgroundColor: theme.palette.background.paper,
+  },
+  privilegeTitle: {
+    fontWeight: 600,
+    fontSize: 14,
+    color: theme.palette.text.primary,
+    margin: 0,
+    marginLeft: theme.spacing(-2),
+  },
+  privileges: {
+    display: 'flex',
+    flex: 1,
+    flexDirection: 'column',
+    height: 'auto',
+    minHeight: 200,
+    width: '100%',
+    borderRadius: theme.shape.borderRadius,
+  },
+
+  privilegeBody: {
+    minWidth: 200,
+  },
+
+  selectAllCheckbox: {
+    marginLeft: 8,
+  },
+  checkbox: {},
+  toggle: {
+    fontSize: 13,
+    '& .toggle-label': {
+      padding: 0,
+      marginRight: theme.spacing(1),
+      fontWeight: '400',
+    },
+    '& .MuiRadio-root': {
+      paddingRight: 8,
+    },
+  },
+}));

+ 1 - 1
server/package.json

@@ -13,7 +13,7 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@json2csv/plainjs": "^7.0.3",
     "@json2csv/plainjs": "^7.0.3",
-    "@zilliz/milvus2-sdk-node": "2.5.3",
+    "@zilliz/milvus2-sdk-node": "2.5.5",
     "axios": "^1.7.7",
     "axios": "^1.7.7",
     "chalk": "4.1.2",
     "chalk": "4.1.2",
     "class-sanitizer": "^1.0.1",
     "class-sanitizer": "^1.0.1",

+ 27 - 0
server/src/collections/collections.controller.ts

@@ -34,6 +34,7 @@ export class CollectionController {
   generateRoutes() {
   generateRoutes() {
     // get all collections
     // get all collections
     this.router.get('/', this.showCollections.bind(this));
     this.router.get('/', this.showCollections.bind(this));
+    this.router.get('/names', this.getCollectionNames.bind(this));
     // get all collections statistics
     // get all collections statistics
     this.router.get('/statistics', this.getStatistics.bind(this));
     this.router.get('/statistics', this.getStatistics.bind(this));
     // index
     // index
@@ -158,6 +159,32 @@ export class CollectionController {
     }
     }
   }
   }
 
 
+  async getCollectionNames(req: Request, res: Response, next: NextFunction) {
+    try {
+      const db_name = req.query?.db_name;
+      const request = {} as any;
+
+      if (db_name) {
+        request.db_name = db_name;
+      }
+
+      const result = await this.collectionsService.showCollections(
+        req.clientId,
+        request
+      );
+      res.send(
+        result.data
+          .sort((a, b) => {
+             // sort by name
+            return a.name.localeCompare(b.name);
+          })
+          .map((item: any) => item.name)
+      );
+    } catch (error) {
+      next(error);
+    }
+  }
+
   async getStatistics(req: Request, res: Response, next: NextFunction) {
   async getStatistics(req: Request, res: Response, next: NextFunction) {
     try {
     try {
       const result = await this.collectionsService.getStatistics(
       const result = await this.collectionsService.getStatistics(

+ 1 - 32
server/src/collections/collections.service.ts

@@ -9,7 +9,6 @@ import {
   RenameCollectionReq,
   RenameCollectionReq,
   AlterAliasReq,
   AlterAliasReq,
   CreateAliasReq,
   CreateAliasReq,
-  DropAliasReq,
   ShowCollectionsReq,
   ShowCollectionsReq,
   ShowCollectionsType,
   ShowCollectionsType,
   DeleteEntitiesReq,
   DeleteEntitiesReq,
@@ -32,7 +31,6 @@ import {
 } from '@zilliz/milvus2-sdk-node';
 } from '@zilliz/milvus2-sdk-node';
 import { Parser } from '@json2csv/plainjs';
 import { Parser } from '@json2csv/plainjs';
 import {
 import {
-  throwErrorFromSDK,
   findKeyValue,
   findKeyValue,
   getKeyValueListFromJsonString,
   getKeyValueListFromJsonString,
   genRows,
   genRows,
@@ -65,7 +63,6 @@ export class CollectionsService {
   async showCollections(clientId: string, data?: ShowCollectionsReq) {
   async showCollections(clientId: string, data?: ShowCollectionsReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.showCollections(data);
     const res = await milvusClient.showCollections(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
@@ -78,7 +75,6 @@ export class CollectionsService {
       data.db_name
       data.db_name
     )) as CollectionFullObject[];
     )) as CollectionFullObject[];
 
 
-    throwErrorFromSDK(res);
     return newCollection[0];
     return newCollection[0];
   }
   }
 
 
@@ -92,7 +88,6 @@ export class CollectionsService {
       collection_name,
       collection_name,
       db_name,
       db_name,
     });
     });
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
@@ -107,8 +102,6 @@ export class CollectionsService {
       collection_name: data.collection_name,
       collection_name: data.collection_name,
     });
     });
 
 
-    throwErrorFromSDK(res.status);
-
     const vectorFields: FieldObject[] = [];
     const vectorFields: FieldObject[] = [];
     const scalarFields: FieldObject[] = [];
     const scalarFields: FieldObject[] = [];
     const functionFields: FieldObject[] = [];
     const functionFields: FieldObject[] = [];
@@ -200,7 +193,6 @@ export class CollectionsService {
   async renameCollection(clientId: string, data: RenameCollectionReq) {
   async renameCollection(clientId: string, data: RenameCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.renameCollection(data);
     const res = await milvusClient.renameCollection(data);
-    throwErrorFromSDK(res);
 
 
     const newCollection = (await this.getAllCollections(
     const newCollection = (await this.getAllCollections(
       clientId,
       clientId,
@@ -213,8 +205,7 @@ export class CollectionsService {
 
 
   async alterCollection(clientId: string, data: AlterCollectionReq) {
   async alterCollection(clientId: string, data: AlterCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
-    const res = await milvusClient.alterCollection(data);
-    throwErrorFromSDK(res);
+    const res = await milvusClient.alterCollectionProperties(data);
 
 
     const newCollection = (await this.getAllCollections(
     const newCollection = (await this.getAllCollections(
       clientId,
       clientId,
@@ -228,14 +219,12 @@ export class CollectionsService {
   async dropCollection(clientId: string, data: DropCollectionReq) {
   async dropCollection(clientId: string, data: DropCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.dropCollection(data);
     const res = await milvusClient.dropCollection(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
   async loadCollection(clientId: string, data: LoadCollectionReq) {
   async loadCollection(clientId: string, data: LoadCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.loadCollection(data);
     const res = await milvusClient.loadCollection(data);
-    throwErrorFromSDK(res);
 
 
     return data.collection_name;
     return data.collection_name;
   }
   }
@@ -243,7 +232,6 @@ export class CollectionsService {
   async loadCollectionAsync(clientId: string, data: LoadCollectionReq) {
   async loadCollectionAsync(clientId: string, data: LoadCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.loadCollectionAsync(data);
     const res = await milvusClient.loadCollectionAsync(data);
-    throwErrorFromSDK(res);
 
 
     return data.collection_name;
     return data.collection_name;
   }
   }
@@ -251,7 +239,6 @@ export class CollectionsService {
   async releaseCollection(clientId: string, data: ReleaseLoadCollectionReq) {
   async releaseCollection(clientId: string, data: ReleaseLoadCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.releaseCollection(data);
     const res = await milvusClient.releaseCollection(data);
-    throwErrorFromSDK(res);
 
 
     // emit update to client
     // emit update to client
     this.updateCollectionsDetails(
     this.updateCollectionsDetails(
@@ -269,14 +256,12 @@ export class CollectionsService {
   ) {
   ) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getCollectionStatistics(data);
     const res = await milvusClient.getCollectionStatistics(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
   async getLoadState(clientId: string, data: GetLoadStateReq) {
   async getLoadState(clientId: string, data: GetLoadStateReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getLoadState(data);
     const res = await milvusClient.getLoadState(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
@@ -307,21 +292,18 @@ export class CollectionsService {
   async insert(clientId: string, data: InsertReq) {
   async insert(clientId: string, data: InsertReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.insert(data);
     const res = await milvusClient.insert(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
   async upsert(clientId: string, data: InsertReq) {
   async upsert(clientId: string, data: InsertReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.upsert(data);
     const res = await milvusClient.upsert(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
   async deleteEntities(clientId: string, data: DeleteEntitiesReq) {
   async deleteEntities(clientId: string, data: DeleteEntitiesReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.deleteEntities(data);
     const res = await milvusClient.deleteEntities(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
@@ -354,7 +336,6 @@ export class CollectionsService {
     );
     );
     const after = Date.now();
     const after = Date.now();
 
 
-    throwErrorFromSDK(res.status);
     Object.assign(res, { latency: after - now });
     Object.assign(res, { latency: after - now });
     return res;
     return res;
   }
   }
@@ -362,7 +343,6 @@ export class CollectionsService {
   async createAlias(clientId: string, data: CreateAliasReq) {
   async createAlias(clientId: string, data: CreateAliasReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.createAlias(data);
     const res = await milvusClient.createAlias(data);
-    throwErrorFromSDK(res);
 
 
     const newCollection = (await this.getAllCollections(
     const newCollection = (await this.getAllCollections(
       clientId,
       clientId,
@@ -376,14 +356,12 @@ export class CollectionsService {
   async alterAlias(clientId: string, data: AlterAliasReq) {
   async alterAlias(clientId: string, data: AlterAliasReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.alterAlias(data);
     const res = await milvusClient.alterAlias(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
   async dropAlias(clientId: string, collection_name: string, data: any) {
   async dropAlias(clientId: string, collection_name: string, data: any) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.dropAlias(data);
     const res = await milvusClient.dropAlias(data);
-    throwErrorFromSDK(res);
 
 
     const newCollection = (await this.getAllCollections(
     const newCollection = (await this.getAllCollections(
       clientId,
       clientId,
@@ -412,7 +390,6 @@ export class CollectionsService {
 
 
     const after = Date.now();
     const after = Date.now();
 
 
-    throwErrorFromSDK(res.status);
     Object.assign(res, { latency: after - now });
     Object.assign(res, { latency: after - now });
     return res;
     return res;
   }
   }
@@ -702,14 +679,12 @@ export class CollectionsService {
   async getCompactionState(clientId: string, data: GetCompactionStateReq) {
   async getCompactionState(clientId: string, data: GetCompactionStateReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getCompactionState(data);
     const res = await milvusClient.getCompactionState(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
   async getQuerySegmentInfo(clientId: string, data: GetQuerySegmentInfoReq) {
   async getQuerySegmentInfo(clientId: string, data: GetQuerySegmentInfoReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getQuerySegmentInfo(data);
     const res = await milvusClient.getQuerySegmentInfo(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
@@ -719,21 +694,18 @@ export class CollectionsService {
   ) {
   ) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getPersistentSegmentInfo(data);
     const res = await milvusClient.getPersistentSegmentInfo(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
   async compact(clientId: string, data: CompactReq) {
   async compact(clientId: string, data: CompactReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.compact(data);
     const res = await milvusClient.compact(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
   async hasCollection(clientId: string, data: HasCollectionReq) {
   async hasCollection(clientId: string, data: HasCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.hasCollection(data);
     const res = await milvusClient.hasCollection(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
@@ -778,7 +750,6 @@ export class CollectionsService {
   async createIndex(clientId: string, data: CreateIndexReq) {
   async createIndex(clientId: string, data: CreateIndexReq) {
     const { milvusClient, indexCache, database } = clientCache.get(clientId);
     const { milvusClient, indexCache, database } = clientCache.get(clientId);
     const res = await milvusClient.createIndex(data);
     const res = await milvusClient.createIndex(data);
-    throwErrorFromSDK(res);
     const key = `${database}/${data.collection_name}`;
     const key = `${database}/${data.collection_name}`;
     // clear cache;
     // clear cache;
     indexCache.delete(key);
     indexCache.delete(key);
@@ -790,7 +761,6 @@ export class CollectionsService {
       data.db_name
       data.db_name
     )) as CollectionFullObject[];
     )) as CollectionFullObject[];
 
 
-    throwErrorFromSDK(res);
     return newCollection[0];
     return newCollection[0];
   }
   }
 
 
@@ -847,7 +817,6 @@ export class CollectionsService {
   async dropIndex(clientId: string, data: DropIndexReq) {
   async dropIndex(clientId: string, data: DropIndexReq) {
     const { milvusClient, indexCache, database } = clientCache.get(clientId);
     const { milvusClient, indexCache, database } = clientCache.get(clientId);
     const res = await milvusClient.dropIndex(data);
     const res = await milvusClient.dropIndex(data);
-    throwErrorFromSDK(res);
 
 
     const key = `${database}/${data.collection_name}`;
     const key = `${database}/${data.collection_name}`;
 
 

+ 2 - 8
server/src/database/databases.service.ts

@@ -6,7 +6,6 @@ import {
   DropDatabasesRequest,
   DropDatabasesRequest,
   AlterDatabaseRequest,
   AlterDatabaseRequest,
 } from '@zilliz/milvus2-sdk-node';
 } from '@zilliz/milvus2-sdk-node';
-import { throwErrorFromSDK } from '../utils/Error';
 import { clientCache } from '../app';
 import { clientCache } from '../app';
 import { DatabaseObject } from '../types';
 import { DatabaseObject } from '../types';
 import { SimpleQueue } from '../utils';
 import { SimpleQueue } from '../utils';
@@ -16,7 +15,6 @@ export class DatabasesService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.createDatabase(data);
     const res = await milvusClient.createDatabase(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
@@ -24,7 +22,6 @@ export class DatabasesService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.describeDatabase(data);
     const res = await milvusClient.describeDatabase(data);
-    throwErrorFromSDK(res.status);
     return res as DescribeDatabaseResponse;
     return res as DescribeDatabaseResponse;
   }
   }
 
 
@@ -55,7 +52,7 @@ export class DatabasesService {
           });
           });
         } catch (e) {
         } catch (e) {
           // ignore
           // ignore
-          console.log('error', e);
+          console.warn('error', e.details);
         }
         }
 
 
         availableDatabases.push({
         availableDatabases.push({
@@ -66,14 +63,13 @@ export class DatabasesService {
         });
         });
       } catch (e) {
       } catch (e) {
         // ignore
         // ignore
-        console.log('error', e);
+        console.warn('error', e.details);
       }
       }
     }
     }
 
 
     // recover current database
     // recover current database
     await milvusClient.use({ db_name: database });
     await milvusClient.use({ db_name: database });
 
 
-    throwErrorFromSDK(res.status);
     return availableDatabases;
     return availableDatabases;
   }
   }
 
 
@@ -81,7 +77,6 @@ export class DatabasesService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.dropDatabase(data);
     const res = await milvusClient.dropDatabase(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
@@ -104,7 +99,6 @@ export class DatabasesService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.alterDatabase(data);
     const res = await milvusClient.alterDatabase(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 }
 }

+ 10 - 4
server/src/middleware/index.ts

@@ -37,7 +37,7 @@ export const ReqHeaderMiddleware = (
 
 
   if (!bypass && !hasClient) {
   if (!bypass && !hasClient) {
     throw HttpErrors(
     throw HttpErrors(
-      HTTP_STATUS_CODE.FORBIDDEN,
+      HTTP_STATUS_CODE.UNAUTHORIZED,
       'Can not find your connection, please reconnect.'
       'Can not find your connection, please reconnect.'
     );
     );
   } else {
   } else {
@@ -52,7 +52,6 @@ export const TransformResMiddleware = (
 ) => {
 ) => {
   const oldSend = res.json;
   const oldSend = res.json;
   res.json = data => {
   res.json = data => {
-    // console.log(data); // do something with the data
     const statusCode = data?.statusCode;
     const statusCode = data?.statusCode;
     const message = data?.message;
     const message = data?.message;
     const error = data?.error;
     const error = data?.error;
@@ -75,7 +74,7 @@ export const ErrorMiddleware = (
   res: Response,
   res: Response,
   next: NextFunction
   next: NextFunction
 ) => {
 ) => {
-  const statusCode = err.statusCode || 500;
+  let statusCode = err.statusCode || 500;
   if (!isElectron()) {
   if (!isElectron()) {
     console.log(
     console.log(
       chalk.blue.bold(req.method, req.url),
       chalk.blue.bold(req.method, req.url),
@@ -89,10 +88,17 @@ export const ErrorMiddleware = (
     return next(err);
     return next(err);
   }
   }
 
 
+  // handle error from milvus service
+  switch (err.code) {
+    case 7:
+      statusCode = HTTP_STATUS_CODE.FORBIDDEN;
+      break;
+  }
+
   if (err) {
   if (err) {
     res
     res
       .status(statusCode)
       .status(statusCode)
-      .json({ message: `${err}`, error: 'Bad Request', statusCode });
+      .json({ message: `${err.details || err.message}`, statusCode });
   }
   }
   next();
   next();
 };
 };

+ 8 - 2
server/src/milvus/milvus.service.ts

@@ -6,10 +6,16 @@ import {
   CONNECT_STATUS,
   CONNECT_STATUS,
 } from '@zilliz/milvus2-sdk-node';
 } from '@zilliz/milvus2-sdk-node';
 import { LRUCache } from 'lru-cache';
 import { LRUCache } from 'lru-cache';
-import { DEFAULT_MILVUS_PORT, INDEX_TTL, SimpleQueue } from '../utils';
+import {
+  DEFAULT_MILVUS_PORT,
+  INDEX_TTL,
+  SimpleQueue,
+  HTTP_STATUS_CODE,
+} from '../utils';
 import { clientCache } from '../app';
 import { clientCache } from '../app';
 import { DescribeIndexRes, AuthReq, AuthObject } from '../types';
 import { DescribeIndexRes, AuthReq, AuthObject } from '../types';
 import { cronsManager } from '../crons';
 import { cronsManager } from '../crons';
+import HttpErrors from 'http-errors';
 
 
 export class MilvusService {
 export class MilvusService {
   private DEFAULT_DATABASE = 'default';
   private DEFAULT_DATABASE = 'default';
@@ -88,7 +94,7 @@ export class MilvusService {
       } catch (error) {
       } catch (error) {
         // If the connection fails, clear the cache and throw an error
         // If the connection fails, clear the cache and throw an error
         clientCache.delete(milvusClient.clientId);
         clientCache.delete(milvusClient.clientId);
-        throw new Error('Failed to connect to Milvus: ' + error);
+        throw HttpErrors(HTTP_STATUS_CODE.UNAUTHORIZED, error);
       }
       }
 
 
       // Check the health of the Milvus server
       // Check the health of the Milvus server

+ 0 - 7
server/src/partitions/partitions.service.ts

@@ -6,7 +6,6 @@ import {
   ReleasePartitionsReq,
   ReleasePartitionsReq,
   ShowPartitionsReq,
   ShowPartitionsReq,
 } from '@zilliz/milvus2-sdk-node';
 } from '@zilliz/milvus2-sdk-node';
-import { throwErrorFromSDK } from '../utils/Error';
 import { findKeyValue } from '../utils/Helper';
 import { findKeyValue } from '../utils/Helper';
 import { ROW_COUNT } from '../utils';
 import { ROW_COUNT } from '../utils';
 import { clientCache } from '../app';
 import { clientCache } from '../app';
@@ -40,7 +39,6 @@ export class PartitionsService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.showPartitions(data);
     const res = await milvusClient.showPartitions(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
@@ -48,7 +46,6 @@ export class PartitionsService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.createPartition(data);
     const res = await milvusClient.createPartition(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
@@ -56,7 +53,6 @@ export class PartitionsService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.dropPartition(data);
     const res = await milvusClient.dropPartition(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
@@ -67,7 +63,6 @@ export class PartitionsService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.getPartitionStatistics(data);
     const res = await milvusClient.getPartitionStatistics(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
@@ -75,7 +70,6 @@ export class PartitionsService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.loadPartitions(data);
     const res = await milvusClient.loadPartitions(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
@@ -83,7 +77,6 @@ export class PartitionsService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.releasePartitions(data);
     const res = await milvusClient.releasePartitions(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 }
 }

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

@@ -7,5 +7,45 @@ import {
 
 
 export type Users = ListCredUsersResponse;
 export type Users = ListCredUsersResponse;
 export type UsersWithRoles = SelectRoleResponse;
 export type UsersWithRoles = SelectRoleResponse;
+export type UserWithRoles = {
+  username: string;
+  roles: string[];
+};
+
+export type RolesWithPrivileges = {
+  roleName: string;
+  privileges: DBCollectionsPrivileges;
+};
+
 export type PrivilegeGroupsRes = ListPrivilegeGroupsResponse;
 export type PrivilegeGroupsRes = ListPrivilegeGroupsResponse;
 export type PrivilegeGroup = PrivelegeGroup;
 export type PrivilegeGroup = PrivelegeGroup;
+
+export type Privilege = {
+  [key: string]: boolean; // key: privilege name, value: whether it's selected
+};
+
+export type CollectionPrivileges = {
+  [collectionValue: string]: Privilege; // key: collection value, value: privileges
+};
+
+export type DBPrivileges = {
+  collections: CollectionPrivileges; // Collection-level privileges
+};
+
+export type DBCollectionsPrivileges = {
+  [dbValue: string]: DBPrivileges; // key: DB value, value: DB privileges and collections
+};
+
+export type RBACOptions = {
+  ClusterPrivilegeGroups: Record<string, unknown>;
+  DatabasePrivilegeGroups: Record<string, unknown>;
+  CollectionPrivilegeGroups: Record<string, unknown>;
+  CustomPrivilegeGroups: Record<string, unknown>;
+  CollectionPrivileges: Record<string, unknown>;
+  DatabasePrivileges: Record<string, unknown>;
+  EntityPrivileges: Record<string, unknown>;
+  IndexPrivileges: Record<string, unknown>;
+  PartitionPrivileges: Record<string, unknown>;
+  RBACPrivileges: Record<string, unknown>;
+  ResourceManagementPrivileges: Record<string, unknown>;
+};

+ 230 - 35
server/src/users/users.controller.ts

@@ -11,7 +11,19 @@ import {
   CreatePrivilegeGroupDto,
   CreatePrivilegeGroupDto,
   PrivilegeToRoleDto,
   PrivilegeToRoleDto,
 } from './dto';
 } from './dto';
-import { OperateRolePrivilegeReq } from '@zilliz/milvus2-sdk-node';
+import type {
+  DBCollectionsPrivileges,
+  RolesWithPrivileges,
+} from '../types/users.type';
+import {
+  DatabasePrivileges,
+  CollectionPrivileges,
+  PartitionPrivileges,
+  IndexPrivileges,
+  EntityPrivileges,
+  ResourceManagementPrivileges,
+  RBACPrivileges,
+} from '../utils';
 
 
 export class UserController {
 export class UserController {
   private router: Router;
   private router: Router;
@@ -49,7 +61,10 @@ export class UserController {
 
 
     // role
     // role
     this.router.get('/rbac', this.rbac.bind(this));
     this.router.get('/rbac', this.rbac.bind(this));
-    this.router.get('/privilegeGroups', this.allPrivilegeGroups.bind(this));
+    this.router.get(
+      '/privilegeGroups',
+      this.getDefaultPriviegGroups.bind(this)
+    );
     this.router.get('/roles', this.getRoles.bind(this));
     this.router.get('/roles', this.getRoles.bind(this));
     this.router.post(
     this.router.post(
       '/roles',
       '/roles',
@@ -91,7 +106,25 @@ export class UserController {
     try {
     try {
       const result = await this.userService.getUsers(req.clientId);
       const result = await this.userService.getUsers(req.clientId);
 
 
-      res.send(result);
+      const results = [];
+      for (let i = 0; i < result.usernames.length; i++) {
+        const username = result.usernames[i];
+        const roles = await this.userService.selectUser(req.clientId, {
+          username,
+          includeRoleInfo: true,
+        });
+        results.push({
+          username,
+          roles: roles.results[0].roles.map(r => r.name),
+        });
+        if (username === 'root') {
+          // Remove the recently pushed "root" user and insert it at the beginning of the results array
+          const rootUser = results.pop();
+          results.unshift(rootUser);
+        }
+      }
+
+      res.send(results);
     } catch (error) {
     } catch (error) {
       next(error);
       next(error);
     }
     }
@@ -150,17 +183,58 @@ export class UserController {
 
 
   async getRoles(req: Request, res: Response, next: NextFunction) {
   async getRoles(req: Request, res: Response, next: NextFunction) {
     try {
     try {
-      const result = await this.userService.getRoles(req.clientId);
+      // Fetch all roles
+      const rolesResult = await this.userService.getRoles(req.clientId);
 
 
-      for (let i = 0; i < result.results.length; i++) {
-        const { entities } = await this.userService.listGrants(req.clientId, {
-          roleName: result.results[i].role.name,
+      // Initialize the result array
+      const rolesWithPrivileges: RolesWithPrivileges[] = [];
+
+      // Iterate through each role
+      for (let i = 0; i < rolesResult.results.length; i++) {
+        const roleName = rolesResult.results[i].role.name;
+
+        // Fetch grants for the current role
+        const grantsResponse = await this.userService.listGrants(
+          req.clientId,
+          roleName
+        );
+
+        // Initialize the privileges structure for the current role
+        const dbCollectionsPrivileges: DBCollectionsPrivileges = {};
+
+        // Iterate through each grant entity
+        for (const entity of grantsResponse.entities) {
+          const { db_name, object_name, grantor } = entity;
+
+          // Initialize the database entry if it doesn't exist
+          if (!dbCollectionsPrivileges[db_name]) {
+            dbCollectionsPrivileges[db_name] = {
+              collections: {},
+            };
+          }
+
+          // Initialize the collection entry if it doesn't exist
+          if (!dbCollectionsPrivileges[db_name].collections[object_name]) {
+            dbCollectionsPrivileges[db_name].collections[object_name] = {};
+          }
+
+          // Add the privilege to the collection
+          dbCollectionsPrivileges[db_name].collections[object_name][
+            grantor.privilege.name
+          ] = true;
+        }
+
+        // Add the role and its privileges to the result array
+        rolesWithPrivileges.push({
+          roleName,
+          privileges: dbCollectionsPrivileges,
         });
         });
-        result.results[i].entities = entities;
       }
       }
 
 
-      res.send(result);
+      // Send the transformed result
+      res.status(200).json(rolesWithPrivileges);
     } catch (error) {
     } catch (error) {
+      // Pass the error to the error-handling middleware
       next(error);
       next(error);
     }
     }
   }
   }
@@ -268,17 +342,59 @@ export class UserController {
 
 
   async rbac(req: Request, res: Response, next: NextFunction) {
   async rbac(req: Request, res: Response, next: NextFunction) {
     try {
     try {
-      const result = await this.userService.getRBAC();
+      const privilegeGrps = await this.userService.getPriviegGroups(
+        req.clientId
+      );
+
+      const ClusterPrivilegeGroups = {} as any;
+      const DatabasePrivilegeGroups = {} as any;
+      const CollectionPrivilegeGroups = {} as any;
+      const CustomPrivilegeGroups = {} as any;
+
+      privilegeGrps.cluster.forEach((g: any) => {
+        ClusterPrivilegeGroups[g.group_name] = g.group_name;
+      });
+
+      privilegeGrps.db.forEach((g: any) => {
+        DatabasePrivilegeGroups[g.group_name] = g.group_name;
+      });
+
+      privilegeGrps.collection.forEach((g: any) => {
+        CollectionPrivilegeGroups[g.group_name] = g.group_name;
+      });
+
+      privilegeGrps.custom.forEach((g: any) => {
+        CustomPrivilegeGroups[g.group_name] = g.group_name;
+      });
+
+      const result = {
+        ClusterPrivilegeGroups,
+        DatabasePrivilegeGroups,
+        CollectionPrivilegeGroups,
+        CustomPrivilegeGroups,
+        DatabasePrivileges,
+        ResourceManagementPrivileges,
+        RBACPrivileges,
+        CollectionPrivileges,
+        PartitionPrivileges,
+        IndexPrivileges,
+        EntityPrivileges,
+      };
+
       res.send(result);
       res.send(result);
     } catch (error) {
     } catch (error) {
       next(error);
       next(error);
     }
     }
   }
   }
 
 
-  async allPrivilegeGroups(req: Request, res: Response, next: NextFunction) {
+  async getDefaultPriviegGroups(
+    req: Request,
+    res: Response,
+    next: NextFunction
+  ) {
     try {
     try {
-      const result = await this.userService.getAllPrivilegeGroups(req.clientId);
-      res.send(result);
+      const result = await this.userService.getPriviegGroups(req.clientId);
+      res.send(result.default);
     } catch (error) {
     } catch (error) {
       next(error);
       next(error);
     }
     }
@@ -293,7 +409,8 @@ export class UserController {
     try {
     try {
       const result = await this.userService.listGrants(req.clientId, {
       const result = await this.userService.listGrants(req.clientId, {
         roleName,
         roleName,
-      });
+        db_name: '*',
+      } as any);
       res.send(result);
       res.send(result);
     } catch (error) {
     } catch (error) {
       next(error);
       next(error);
@@ -304,33 +421,111 @@ export class UserController {
     req: Request<
     req: Request<
       { roleName: string },
       { roleName: string },
       {},
       {},
-      { privileges: OperateRolePrivilegeReq[] }
+      { privileges: DBCollectionsPrivileges }
     >,
     >,
     res: Response,
     res: Response,
     next: NextFunction
     next: NextFunction
   ) {
   ) {
     const { privileges } = req.body;
     const { privileges } = req.body;
     const { roleName } = req.params;
     const { roleName } = req.params;
+    const clientId = req.clientId;
 
 
     const results = [];
     const results = [];
+    const rollbackStack = []; // Stack to store actions for rollback in case of failure
 
 
     try {
     try {
-      // revoke all
-      await this.userService.revokeAllRolePrivileges(req.clientId, {
+      // check if role exists
+      const hasRole = await this.userService.hasRole(clientId, {
         roleName,
         roleName,
       });
       });
+      // if role does not exist, create it
+      if (hasRole.hasRole === false) {
+        await this.userService.createRole(clientId, { roleName });
+      }
 
 
-      // assign new user roles
-      for (let i = 0; i < privileges.length; i++) {
-        const result = await this.userService.grantRolePrivilege(
-          req.clientId,
-          privileges[i]
-        );
-        results.push(result);
+      // Iterate over each database
+      for (const [dbName, dbPrivileges] of Object.entries(privileges)) {
+        // Iterate over each collection in the database
+        for (const [collectionName, collectionPrivileges] of Object.entries(
+          dbPrivileges.collections
+        )) {
+          // Iterate over each privilege in the collection
+          for (const [privilegeName, isGranted] of Object.entries(
+            collectionPrivileges
+          )) {
+            const requestData = {
+              role: roleName,
+              privilege: privilegeName,
+              db_name: dbName,
+              collection_name: collectionName,
+            };
+
+            let result;
+            try {
+              if (isGranted) {
+                // If the privilege is true, call grantPrivilegeV2
+                result = await this.userService.grantPrivilegeV2(
+                  clientId,
+                  requestData
+                );
+                // Push the reverse action (revoke) to the rollback stack
+                rollbackStack.push({ action: 'revoke', data: requestData });
+              } else {
+                // If the privilege is false, call revokePrivilegeV2
+                result = await this.userService.revokePrivilegeV2(
+                  clientId,
+                  requestData
+                );
+                // Push the reverse action (grant) to the rollback stack
+                rollbackStack.push({ action: 'grant', data: requestData });
+              }
+
+              // Collect the result
+              results.push({
+                dbName,
+                collectionName,
+                privilegeName,
+                isGranted,
+                result,
+              });
+            } catch (error) {
+              // If an error occurs, log it and initiate rollback
+              console.error(
+                `Failed to update privilege: ${privilegeName} for collection: ${collectionName} in database: ${dbName}`,
+                error
+              );
+
+              // Rollback all previously applied changes
+              while (rollbackStack.length > 0) {
+                const { action, data } = rollbackStack.pop();
+                try {
+                  if (action === 'grant') {
+                    await this.userService.grantPrivilegeV2(clientId, data);
+                  } else if (action === 'revoke') {
+                    await this.userService.revokePrivilegeV2(clientId, data);
+                  }
+                } catch (rollbackError) {
+                  console.error(
+                    `Rollback failed for action: ${action} on privilege: ${data.privilege}`,
+                    rollbackError
+                  );
+                }
+              }
+
+              // drop the role if creation fails
+              await this.userService.deleteRole(clientId, { roleName });
+
+              // Propagate the error to the error handler
+              throw error;
+            }
+          }
+        }
       }
       }
 
 
-      res.send(results);
+      // Return the results if everything succeeds
+      res.status(200).json({ results });
     } catch (error) {
     } catch (error) {
+      // Pass the error to the error-handling middleware
       next(error);
       next(error);
     }
     }
   }
   }
@@ -350,7 +545,7 @@ export class UserController {
       // add privileges to the group
       // add privileges to the group
       const result = await this.userService.addPrivilegeToGroup(req.clientId, {
       const result = await this.userService.addPrivilegeToGroup(req.clientId, {
         group_name,
         group_name,
-        priviliges: privileges,
+        privileges,
       });
       });
 
 
       res.send(result);
       res.send(result);
@@ -410,7 +605,7 @@ export class UserController {
     const { privileges } = req.body;
     const { privileges } = req.body;
     // get existing group
     // get existing group
     const theGroup = await this.userService.getPrivilegeGroup(req.clientId, {
     const theGroup = await this.userService.getPrivilegeGroup(req.clientId, {
-      group_name: group_name,
+      group_name,
     });
     });
 
 
     // if no group found, return error
     // if no group found, return error
@@ -421,14 +616,14 @@ export class UserController {
     try {
     try {
       // remove all privileges from the group
       // remove all privileges from the group
       await this.userService.removePrivilegeFromGroup(req.clientId, {
       await this.userService.removePrivilegeFromGroup(req.clientId, {
-        group_name: group_name,
-        priviliges: theGroup.privileges.map(p => p.name),
+        group_name,
+        privileges: theGroup.privileges.map(p => p.name),
       });
       });
 
 
       // add new privileges to the group
       // add new privileges to the group
       const result = await this.userService.addPrivilegeToGroup(req.clientId, {
       const result = await this.userService.addPrivilegeToGroup(req.clientId, {
-        group_name: group_name,
-        priviliges: privileges,
+        group_name,
+        privileges,
       });
       });
 
 
       res.send(result);
       res.send(result);
@@ -445,10 +640,10 @@ export class UserController {
     const { role, collection, privilege } = req.body;
     const { role, collection, privilege } = req.body;
     try {
     try {
       const result = await this.userService.grantPrivilegeV2(req.clientId, {
       const result = await this.userService.grantPrivilegeV2(req.clientId, {
-        role: role,
+        role,
         collection_name: collection || '*',
         collection_name: collection || '*',
         db_name: req.db_name,
         db_name: req.db_name,
-        privilege: privilege,
+        privilege,
       });
       });
       res.send(result);
       res.send(result);
     } catch (error) {
     } catch (error) {
@@ -464,10 +659,10 @@ export class UserController {
     const { role, collection, privilege } = req.body;
     const { role, collection, privilege } = req.body;
     try {
     try {
       const result = await this.userService.revokePrivilegeV2(req.clientId, {
       const result = await this.userService.revokePrivilegeV2(req.clientId, {
-        role: role,
+        role,
         collection_name: collection || '*',
         collection_name: collection || '*',
         db_name: req.db_name,
         db_name: req.db_name,
-        privilege: privilege,
+        privilege,
       });
       });
       res.send(result);
       res.send(result);
     } catch (error) {
     } catch (error) {

+ 73 - 72
server/src/users/users.service.ts

@@ -9,19 +9,10 @@ import {
   HasRoleReq,
   HasRoleReq,
   listRoleReq,
   listRoleReq,
   SelectUserReq,
   SelectUserReq,
-  ListGrantsReq,
   OperateRolePrivilegeReq,
   OperateRolePrivilegeReq,
   GrantPrivilegeV2Request,
   GrantPrivilegeV2Request,
   RevokePrivilegeV2Request,
   RevokePrivilegeV2Request,
 } from '@zilliz/milvus2-sdk-node';
 } from '@zilliz/milvus2-sdk-node';
-import { throwErrorFromSDK } from '../utils/Error';
-import {
-  Privileges,
-  GlobalPrivileges,
-  CollectionPrivileges,
-  UserPrivileges,
-  RbacObjects,
-} from '../utils';
 import { clientCache } from '../app';
 import { clientCache } from '../app';
 
 
 export class UserService {
 export class UserService {
@@ -29,7 +20,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.listUsers();
     const res = await milvusClient.listUsers();
-    throwErrorFromSDK(res.status);
 
 
     return res;
     return res;
   }
   }
@@ -38,7 +28,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.createUser(data);
     const res = await milvusClient.createUser(data);
-    throwErrorFromSDK(res);
 
 
     return res;
     return res;
   }
   }
@@ -47,7 +36,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.updateUser(data);
     const res = await milvusClient.updateUser(data);
-    throwErrorFromSDK(res);
 
 
     return res;
     return res;
   }
   }
@@ -56,7 +44,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.deleteUser(data);
     const res = await milvusClient.deleteUser(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
@@ -64,7 +51,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.listRoles(data);
     const res = await milvusClient.listRoles(data);
-    throwErrorFromSDK(res.status);
 
 
     return res;
     return res;
   }
   }
@@ -73,7 +59,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.selectUser(data);
     const res = await milvusClient.selectUser(data);
-    throwErrorFromSDK(res.status);
 
 
     return res;
     return res;
   }
   }
@@ -82,7 +67,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.createRole(data);
     const res = await milvusClient.createRole(data);
-    throwErrorFromSDK(res);
 
 
     return res;
     return res;
   }
   }
@@ -91,7 +75,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.dropRole(data);
     const res = await milvusClient.dropRole(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
@@ -99,7 +82,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.addUserToRole(data);
     const res = await milvusClient.addUserToRole(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
@@ -107,7 +89,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.removeUserFromRole(data);
     const res = await milvusClient.removeUserFromRole(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
@@ -115,26 +96,13 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.hasRole(data);
     const res = await milvusClient.hasRole(data);
-    throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }
 
 
-  async getRBAC() {
-    return {
-      Privileges,
-      GlobalPrivileges,
-      CollectionPrivileges,
-      UserPrivileges,
-      RbacObjects,
-    };
-  }
-
-  async getAllPrivilegeGroups(clientId: string) {
+  async getPriviegGroups(clientId: string) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
     const privilegeGrps = await milvusClient.listPrivilegeGroups();
     const privilegeGrps = await milvusClient.listPrivilegeGroups();
 
 
-    throwErrorFromSDK(privilegeGrps.status);
-
     const defaultGrp = [
     const defaultGrp = [
       'ClusterAdmin',
       'ClusterAdmin',
       'ClusterReadOnly',
       'ClusterReadOnly',
@@ -149,24 +117,71 @@ export class UserService {
       'CollectionReadWrite',
       'CollectionReadWrite',
     ];
     ];
 
 
-    // only show default groups
-    const groups = privilegeGrps.privilege_groups.filter(
-      g => defaultGrp.indexOf(g.group_name) !== -1
-    );
+    const clusterGrp = ['ClusterAdmin', 'ClusterReadOnly', 'ClusterReadWrite'];
+
+    const databaseGrp = [
+      'DatabaseAdmin',
+      'DatabaseReadOnly',
+      'DatabaseReadWrite',
+    ];
 
 
-    // sort groups by the order in defaultGrp
-    groups.sort(
-      (a, b) =>
-        defaultGrp.indexOf(a.group_name) - defaultGrp.indexOf(b.group_name)
+    const collectionGrp = [
+      'CollectionAdmin',
+      'CollectionReadOnly',
+      'CollectionReadWrite',
+    ];
+
+    let res: Record<string, any> = {};
+
+    ['cluster', 'db', 'collection', 'default', 'custom', 'all'].forEach(
+      type => {
+        let groups = [] as any[];
+        switch (type) {
+          case 'cluster':
+            groups = privilegeGrps.privilege_groups.filter(g =>
+              clusterGrp.includes(g.group_name)
+            );
+            break;
+          case 'db':
+            groups = privilegeGrps.privilege_groups.filter(g =>
+              databaseGrp.includes(g.group_name)
+            );
+            break;
+          case 'collection':
+            groups = privilegeGrps.privilege_groups.filter(g =>
+              collectionGrp.includes(g.group_name)
+            );
+            break;
+          case 'default':
+            groups = privilegeGrps.privilege_groups.filter(g =>
+              defaultGrp.includes(g.group_name)
+            );
+            break;
+          case 'custom':
+            groups = privilegeGrps.privilege_groups.filter(
+              g => !defaultGrp.includes(g.group_name)
+            );
+            break;
+          case 'all':
+            groups = privilegeGrps.privilege_groups;
+            break;
+        }
+
+        res[type] = groups.sort(
+          (a, b) =>
+            defaultGrp.indexOf(a.group_name) - defaultGrp.indexOf(b.group_name)
+        );
+      }
     );
     );
 
 
-    return groups;
+    return res;
   }
   }
 
 
-  async listGrants(clientId: string, data: ListGrantsReq) {
+  async listGrants(clientId: string, roleName: string) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
-    const res = await milvusClient.listGrants(data);
-    throwErrorFromSDK(res.status);
+    const res = await milvusClient.listGrants({
+      roleName,
+    });
     return res;
     return res;
   }
   }
 
 
@@ -174,7 +189,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.grantRolePrivilege(data);
     const res = await milvusClient.grantRolePrivilege(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
@@ -182,24 +196,20 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.revokeRolePrivilege(data);
     const res = await milvusClient.revokeRolePrivilege(data);
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
   async revokeAllRolePrivileges(clientId: string, data: { roleName: string }) {
   async revokeAllRolePrivileges(clientId: string, data: { roleName: string }) {
     // get existing privileges
     // get existing privileges
-    const existingPrivileges = await this.listGrants(clientId, {
-      roleName: data.roleName,
-    });
-
-    // revoke all
-    for (let i = 0; i < existingPrivileges.entities.length; i++) {
-      const res = existingPrivileges.entities[i];
-      await this.revokeRolePrivilege(clientId, {
-        object: res.object.name,
-        objectName: res.object_name,
-        privilegeName: res.grantor.privilege.name,
-        roleName: res.role.name,
+    const existingPrivileges = await this.listGrants(clientId, data.roleName);
+
+    // revoke all existing privileges
+    for (const entity of existingPrivileges.entities) {
+      const res = await this.revokePrivilegeV2(clientId, {
+        db_name: entity.db_name,
+        collection_name: entity.object_name,
+        privilege: entity.grantor.privilege.name,
+        role: entity.role.name,
       });
       });
     }
     }
   }
   }
@@ -212,7 +222,6 @@ export class UserService {
       group_name: data.group_name,
       group_name: data.group_name,
     });
     });
 
 
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
@@ -222,8 +231,6 @@ export class UserService {
 
 
     const res = await milvusClient.listPrivilegeGroups();
     const res = await milvusClient.listPrivilegeGroups();
 
 
-    throwErrorFromSDK(res.status);
-
     return res;
     return res;
   }
   }
 
 
@@ -235,7 +242,6 @@ export class UserService {
       g => g.group_name === data.group_name
       g => g.group_name === data.group_name
     );
     );
 
 
-    throwErrorFromSDK(res.status);
     return group;
     return group;
   }
   }
 
 
@@ -247,39 +253,36 @@ export class UserService {
       group_name: data.group_name,
       group_name: data.group_name,
     });
     });
 
 
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
   // update privilege group
   // update privilege group
   async addPrivilegeToGroup(
   async addPrivilegeToGroup(
     clientId: string,
     clientId: string,
-    data: { group_name: string; priviliges: string[] }
+    data: { group_name: string; privileges: string[] }
   ) {
   ) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.addPrivilegesToGroup({
     const res = await milvusClient.addPrivilegesToGroup({
       group_name: data.group_name,
       group_name: data.group_name,
-      privileges: data.priviliges,
+      privileges: data.privileges,
     });
     });
 
 
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
   // remove privilege from group
   // remove privilege from group
   async removePrivilegeFromGroup(
   async removePrivilegeFromGroup(
     clientId: string,
     clientId: string,
-    data: { group_name: string; priviliges: string[] }
+    data: { group_name: string; privileges: string[] }
   ) {
   ) {
     const { milvusClient } = clientCache.get(clientId);
     const { milvusClient } = clientCache.get(clientId);
 
 
     const res = await milvusClient.removePrivilegesFromGroup({
     const res = await milvusClient.removePrivilegesFromGroup({
       group_name: data.group_name,
       group_name: data.group_name,
-      privileges: data.priviliges,
+      privileges: data.privileges,
     });
     });
 
 
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
@@ -289,7 +292,6 @@ export class UserService {
 
 
     const res = await milvusClient.grantPrivilegeV2(data);
     const res = await milvusClient.grantPrivilegeV2(data);
 
 
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
@@ -299,7 +301,6 @@ export class UserService {
 
 
     const res = await milvusClient.revokePrivilegeV2(data);
     const res = await milvusClient.revokePrivilegeV2(data);
 
 
-    throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 }
 }

+ 40 - 62
server/src/utils/Const.ts

@@ -5,8 +5,6 @@ export const ROW_COUNT = 'row_count';
 export const MILVUS_CLIENT_ID = 'milvus-client-id';
 export const MILVUS_CLIENT_ID = 'milvus-client-id';
 
 
 // for lru cache
 // for lru cache
-export const CLIENT_CACHE = 'insight_cache';
-export const INDEX_CACHE = 'index_cache';
 export const CLIENT_TTL = 1000 * 60 * 60 * 24;
 export const CLIENT_TTL = 1000 * 60 * 60 * 24;
 export const INDEX_TTL = 1000 * 60 * 60;
 export const INDEX_TTL = 1000 * 60 * 60;
 
 
@@ -74,98 +72,77 @@ export enum HTTP_STATUS_CODE {
   HTTP_VERSION_NOT_SUPPORTED = 505,
   HTTP_VERSION_NOT_SUPPORTED = 505,
 }
 }
 
 
-// RBAC: default objects
 export enum RbacObjects {
 export enum RbacObjects {
   Collection = 'Collection',
   Collection = 'Collection',
   Global = 'Global',
   Global = 'Global',
   User = 'User',
   User = 'User',
 }
 }
 
 
-// RBAC: collection privileges
+export enum DatabasePrivileges {
+  CreateDatabase = 'CreateDatabase',
+  DescribeDatabase = 'DescribeDatabase',
+  ListDatabases = 'ListDatabases',
+  DropDatabase = 'DropDatabase',
+  AlterDatabase = 'AlterDatabase',
+}
+
 export enum CollectionPrivileges {
 export enum CollectionPrivileges {
   CreateCollection = 'CreateCollection',
   CreateCollection = 'CreateCollection',
-  DropCollection = 'DropCollection',
   DescribeCollection = 'DescribeCollection',
   DescribeCollection = 'DescribeCollection',
   ShowCollections = 'ShowCollections',
   ShowCollections = 'ShowCollections',
+  DropCollection = 'DropCollection',
   RenameCollection = 'RenameCollection',
   RenameCollection = 'RenameCollection',
-  CreateIndex = 'CreateIndex',
-  DropIndex = 'DropIndex',
-  IndexDetail = 'IndexDetail',
+  CreateAlias = 'CreateAlias',
+  DescribeAlias = 'DescribeAlias',
+  DropAlias = 'DropAlias',
+  ListAliases = 'ListAliases',
   Load = 'Load',
   Load = 'Load',
   GetLoadingProgress = 'GetLoadingProgress',
   GetLoadingProgress = 'GetLoadingProgress',
   GetLoadState = 'GetLoadState',
   GetLoadState = 'GetLoadState',
   Release = 'Release',
   Release = 'Release',
-  Insert = 'Insert',
-  Upsert = 'Upsert',
-  Delete = 'Delete',
-  Search = 'Search',
   Flush = 'Flush',
   Flush = 'Flush',
   GetFlushState = 'GetFlushState',
   GetFlushState = 'GetFlushState',
-  Query = 'Query',
   GetStatistics = 'GetStatistics',
   GetStatistics = 'GetStatistics',
   Compaction = 'Compaction',
   Compaction = 'Compaction',
-  Import = 'Import',
-  LoadBalance = 'LoadBalance',
+  FlushAll = 'FlushAll',
+}
+
+export enum PartitionPrivileges {
   CreatePartition = 'CreatePartition',
   CreatePartition = 'CreatePartition',
   DropPartition = 'DropPartition',
   DropPartition = 'DropPartition',
   ShowPartitions = 'ShowPartitions',
   ShowPartitions = 'ShowPartitions',
   HasPartition = 'HasPartition',
   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',
+export enum EntityPrivileges {
+  Query = 'Query',
+  Insert = 'Insert',
+  Upsert = 'Upsert',
+  Delete = 'Delete',
+  Search = 'Search',
+  Import = 'Import',
 }
 }
 
 
-// RBAC: global privileges
-export enum GlobalPrivileges {
-  All = '*',
-  CreateCollection = 'CreateCollection',
-  DropCollection = 'DropCollection',
-  DescribeCollection = 'DescribeCollection',
-  ShowCollections = 'ShowCollections',
-  RenameCollection = 'RenameCollection',
-  FlushAll = 'FlushAll',
-  CreateOwnership = 'CreateOwnership',
-  DropOwnership = 'DropOwnership',
-  SelectOwnership = 'SelectOwnership',
-  ManageOwnership = 'ManageOwnership',
+export enum ResourceManagementPrivileges {
   CreateResourceGroup = 'CreateResourceGroup',
   CreateResourceGroup = 'CreateResourceGroup',
   DropResourceGroup = 'DropResourceGroup',
   DropResourceGroup = 'DropResourceGroup',
+  UpdateResourceGroups = 'UpdateResourceGroups',
   DescribeResourceGroup = 'DescribeResourceGroup',
   DescribeResourceGroup = 'DescribeResourceGroup',
   ListResourceGroups = 'ListResourceGroups',
   ListResourceGroups = 'ListResourceGroups',
+  LoadBalance = 'LoadBalance',
   TransferNode = 'TransferNode',
   TransferNode = 'TransferNode',
   TransferReplica = 'TransferReplica',
   TransferReplica = 'TransferReplica',
-  CreateDatabase = 'CreateDatabase',
-  ListDatabases = 'ListDatabases',
-  DropDatabase = 'DropDatabase',
-  CreateAlias = 'CreateAlias',
-  DropAlias = 'DropAlias',
-  DescribeAlias = 'DescribeAlias',
-  ListAliases = 'ListAliases',
+  BackupRBAC = 'BackupRBAC',
+  RestoreRBAC = 'RestoreRBAC',
 }
 }
 
 
-// RBAC: resource group privileges
-export enum ResourceGroupPrivileges {
-  CreateResourceGroup = 'CreateResourceGroup',
-  DropResourceGroup = 'DropResourceGroup',
-  DescribeResourceGroup = 'DescribeResourceGroup',
-  ListResourceGroups = 'ListResourceGroups',
-  UpdateResourceGroups = 'UpdateResourceGroups',
-  TransferNode = 'TransferNode',
-  TransferReplica = 'TransferReplica',
+export enum IndexPrivileges {
+  CreateIndex = 'CreateIndex',
+  DropIndex = 'DropIndex',
+  IndexDetail = 'IndexDetail',
 }
 }
 
 
-// RBAC: user privileges
-export enum UserPrivileges {
+export enum RBACPrivileges {
   UpdateUser = 'UpdateUser',
   UpdateUser = 'UpdateUser',
   SelectUser = 'SelectUser',
   SelectUser = 'SelectUser',
   SelectOwnership = 'SelectOwnership',
   SelectOwnership = 'SelectOwnership',
@@ -176,16 +153,17 @@ export enum UserPrivileges {
   DropPrivilegeGroup = 'DropPrivilegeGroup',
   DropPrivilegeGroup = 'DropPrivilegeGroup',
   ListPrivilegeGroups = 'ListPrivilegeGroups',
   ListPrivilegeGroups = 'ListPrivilegeGroups',
   OperatePrivilegeGroup = 'OperatePrivilegeGroup',
   OperatePrivilegeGroup = 'OperatePrivilegeGroup',
-  RestoreRBAC = 'RestoreRBAC',
-  BackupRBAC = 'BackupRBAC',
 }
 }
 
 
 // RBAC: all privileges
 // RBAC: all privileges
 export const Privileges = {
 export const Privileges = {
-  ...CollectionPrivileges,
   ...DatabasePrivileges,
   ...DatabasePrivileges,
-  ...ResourceGroupPrivileges,
-  ...UserPrivileges,
+  ...CollectionPrivileges,
+  ...PartitionPrivileges,
+  ...IndexPrivileges,
+  ...EntityPrivileges,
+  ...ResourceManagementPrivileges,
+  ...RBACPrivileges,
 };
 };
 
 
 export enum LOADING_STATE {
 export enum LOADING_STATE {

+ 2 - 2
server/src/utils/Error.ts

@@ -1,7 +1,7 @@
-import { ErrorCode, ResStatus } from '@zilliz/milvus2-sdk-node/dist/milvus';
+import { ErrorCode, ResStatus } from '@zilliz/milvus2-sdk-node';
 
 
 export const throwErrorFromSDK = (res: ResStatus) => {
 export const throwErrorFromSDK = (res: ResStatus) => {
   if (res.error_code !== ErrorCode.SUCCESS) {
   if (res.error_code !== ErrorCode.SUCCESS) {
-    throw res.reason;
+    // throw res.reason;
   }
   }
 };
 };

+ 10 - 4
server/yarn.lock

@@ -998,6 +998,11 @@
     mkdirp "^1.0.4"
     mkdirp "^1.0.4"
     rimraf "^3.0.2"
     rimraf "^3.0.2"
 
 
+"@opentelemetry/api@^1.9.0":
+  version "1.9.0"
+  resolved "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe"
+  integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
+
 "@petamoriken/float16@^3.8.6":
 "@petamoriken/float16@^3.8.6":
   version "3.8.6"
   version "3.8.6"
   resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.8.6.tgz#580701cb97a510882342333d31c7cbfd9e14b4f4"
   resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.8.6.tgz#580701cb97a510882342333d31c7cbfd9e14b4f4"
@@ -1476,13 +1481,14 @@
   resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.9.tgz#b6ef7457e826be8049667ae673eda7876eb049be"
   resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.9.tgz#b6ef7457e826be8049667ae673eda7876eb049be"
   integrity sha512-4VSbbcMoxc4KLjb1gs96SRmi7w4h1SF+fCoiK0XaQX62buCc1G5d0DC5bJ9xJBNPDSVCmIrcl8BiYxzjrqaaJA==
   integrity sha512-4VSbbcMoxc4KLjb1gs96SRmi7w4h1SF+fCoiK0XaQX62buCc1G5d0DC5bJ9xJBNPDSVCmIrcl8BiYxzjrqaaJA==
 
 
-"@zilliz/milvus2-sdk-node@2.5.3":
-  version "2.5.3"
-  resolved "https://registry.yarnpkg.com/@zilliz/milvus2-sdk-node/-/milvus2-sdk-node-2.5.3.tgz#3ea5883c584de691eed36eeb602eee472069831e"
-  integrity sha512-8rLs/E0uukHdV1TEe5zMJiL+l6cmqgcYgmprlSbQLvF8ZBDl7rZsxayy94yNMTAxtrzwV5EUCvhd6aPZeO7FSw==
+"@zilliz/milvus2-sdk-node@2.5.5":
+  version "2.5.5"
+  resolved "https://registry.npmjs.org/@zilliz/milvus2-sdk-node/-/milvus2-sdk-node-2.5.5.tgz#86f2d37a44c3b63949bd1be07c33a46b0bf0d73d"
+  integrity sha512-vKDbNu6auOTV0PZSvjKhdXR4wEs+ldREmYA3Ffll2kqJxaljAXmTcDXCw7arFa+/n+xDsg+nP1KTpuZATYDZ8g==
   dependencies:
   dependencies:
     "@grpc/grpc-js" "^1.12.1"
     "@grpc/grpc-js" "^1.12.1"
     "@grpc/proto-loader" "^0.7.10"
     "@grpc/proto-loader" "^0.7.10"
+    "@opentelemetry/api" "^1.9.0"
     "@petamoriken/float16" "^3.8.6"
     "@petamoriken/float16" "^3.8.6"
     dayjs "^1.11.7"
     dayjs "^1.11.7"
     generic-pool "^3.9.0"
     generic-pool "^3.9.0"