From ff1020ccfb3fd7493c91c3b674c369ee9472d10f Mon Sep 17 00:00:00 2001 From: Jin Hai Date: Thu, 1 Jan 2026 12:49:34 +0800 Subject: [PATCH] ADMIN CLI: support grant/revoke user admin authorization (#12381) ### What problem does this PR solve? ``` admin> grant admin 'aaa@aaa1.com'; Fail to grant aaa@aaa1.com admin authorization, code: 404, message: User 'aaa@aaa1.com' not found admin> grant admin 'aaa@aaa.com'; Grant successfully! admin> revoke admin 'aaa1@aaa.com'; Fail to revoke aaa1@aaa.com admin authorization, code: 404, message: User 'aaa1@aaa.com' not found admin> revoke admin 'aaa@aaa.com'; Revoke successfully! admin> revoke admin 'aaa@aaa.com'; aaa@aaa.com isn't superuser, yet! admin> grant admin 'aaa@aaa.com'; Grant successfully! admin> grant admin 'aaa@aaa.com'; aaa@aaa.com is already superuser! admin> revoke admin 'aaa@aaa.com'; Revoke successfully! ``` ### Type of change - [x] New Feature (non-breaking change which adds functionality) Signed-off-by: Jin Hai Co-authored-by: Kevin Hu --- admin/client/admin_client.py | 45 ++++++++++++++++++++++++++++++++++++ admin/server/routes.py | 30 ++++++++++++++++++++++++ admin/server/services.py | 32 +++++++++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/admin/client/admin_client.py b/admin/client/admin_client.py index f70e1624e..f0701bf7c 100644 --- a/admin/client/admin_client.py +++ b/admin/client/admin_client.py @@ -53,6 +53,8 @@ sql_command: list_services | alter_user_role | show_user_permission | show_version + | grant_admin + | revoke_admin // meta command definition meta_command: "\\" meta_command_name [meta_args] @@ -77,6 +79,7 @@ DROP: "DROP"i USER: "USER"i ALTER: "ALTER"i ACTIVE: "ACTIVE"i +ADMIN: "ADMIN"i PASSWORD: "PASSWORD"i DATASETS: "DATASETS"i OF: "OF"i @@ -123,6 +126,9 @@ revoke_permission: REVOKE action_list ON identifier FROM ROLE identifier ";" alter_user_role: ALTER USER quoted_string SET ROLE identifier ";" show_user_permission: SHOW USER PERMISSION quoted_string ";" +grant_admin: GRANT ADMIN quoted_string ";" +revoke_admin: REVOKE ADMIN quoted_string ";" + show_version: SHOW VERSION ";" action_list: identifier ("," identifier)* @@ -249,6 +255,14 @@ class AdminTransformer(Transformer): def show_version(self, items): return {"type": "show_version"} + def grant_admin(self, items): + user_name = items[2] + return {"type": "grant_admin", "user_name": user_name} + + def revoke_admin(self, items): + user_name = items[2] + return {"type": "revoke_admin", "user_name": user_name} + def action_list(self, items): return items @@ -566,6 +580,10 @@ class AdminCLI(Cmd): self._show_user_permission(command_dict) case "show_version": self._show_version(command_dict) + case "grant_admin": + self._grant_admin(command_dict) + case "revoke_admin": + self._revoke_admin(command_dict) case "meta": self._handle_meta_command(command_dict) case _: @@ -698,6 +716,33 @@ class AdminCLI(Cmd): else: print(f"Unknown activate status: {activate_status}.") + + def _grant_admin(self, command): + user_name_tree: Tree = command["user_name"] + user_name: str = user_name_tree.children[0].strip("'\"") + url = f"http://{self.host}:{self.port}/api/v1/admin/users/{user_name}/admin" + # print(f"Grant admin: {url}") + # return + response = self.session.put(url) + res_json = response.json() + if response.status_code == 200: + print(res_json["message"]) + else: + print(f"Fail to grant {user_name} admin authorization, code: {res_json['code']}, message: {res_json['message']}") + + def _revoke_admin(self, command): + user_name_tree: Tree = command["user_name"] + user_name: str = user_name_tree.children[0].strip("'\"") + url = f"http://{self.host}:{self.port}/api/v1/admin/users/{user_name}/admin" + # print(f"Revoke admin: {url}") + # return + response = self.session.delete(url) + res_json = response.json() + if response.status_code == 200: + print(res_json["message"]) + else: + print(f"Fail to revoke {user_name} admin authorization, code: {res_json['code']}, message: {res_json['message']}") + def _handle_list_datasets(self, command): username_tree: Tree = command["user_name"] user_name: str = username_tree.children[0].strip("'\"") diff --git a/admin/server/routes.py b/admin/server/routes.py index e83f3ff08..2d0e771ce 100644 --- a/admin/server/routes.py +++ b/admin/server/routes.py @@ -158,6 +158,36 @@ def alter_user_activate_status(username): return error_response(str(e), 500) +@admin_bp.route('/users//admin', methods=['PUT']) +@login_required +@check_admin_auth +def grant_admin(username): + try: + if current_user.email == username: + return error_response(f"can't grant current user: {username}", 409) + msg = UserMgr.grant_admin(username) + return success_response(None, msg) + + except AdminException as e: + return error_response(e.message, e.code) + except Exception as e: + return error_response(str(e), 500) + +@admin_bp.route('/users//admin', methods=['DELETE']) +@login_required +@check_admin_auth +def revoke_admin(username): + try: + if current_user.email == username: + return error_response(f"can't grant current user: {username}", 409) + msg = UserMgr.revoke_admin(username) + return success_response(None, msg) + + except AdminException as e: + return error_response(e.message, e.code) + except Exception as e: + return error_response(str(e), 500) + @admin_bp.route('/users/', methods=['GET']) @login_required @check_admin_auth diff --git a/admin/server/services.py b/admin/server/services.py index c394dae3a..3cb908ebc 100644 --- a/admin/server/services.py +++ b/admin/server/services.py @@ -137,6 +137,38 @@ class UserMgr: UserService.update_user(usr.id, {"is_active": target_status}) return f"Turn {_activate_status} user activate status successfully!" + @staticmethod + def grant_admin(username: str): + # use email to find user. check exist and unique. + user_list = UserService.query_user_by_email(username) + if not user_list: + raise UserNotFoundError(username) + elif len(user_list) > 1: + raise AdminException(f"Exist more than 1 user: {username}!") + # check activate status different from new + usr = user_list[0] + if usr.is_superuser: + return f"{usr} is already superuser!" + # update is_active + UserService.update_user(usr.id, {"is_superuser": True}) + return "Grant successfully!" + + @staticmethod + def revoke_admin(username: str): + # use email to find user. check exist and unique. + user_list = UserService.query_user_by_email(username) + if not user_list: + raise UserNotFoundError(username) + elif len(user_list) > 1: + raise AdminException(f"Exist more than 1 user: {username}!") + # check activate status different from new + usr = user_list[0] + if not usr.is_superuser: + return f"{usr} isn't superuser, yet!" + # update is_active + UserService.update_user(usr.id, {"is_superuser": False}) + return "Revoke successfully!" + class UserServiceMgr: