diff --git a/README.md b/README.md index 601162914f..f3152b4929 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ | Redis | √ | × | √ | × | × | × | × | × | × | × | | PgSQL | √ | × | √ | × | × | × | × | × | × | × | | Oracle | √ | √ | √ | √ | √ | × | × | × | × | × | -| MongoDB | √ | √ | √ | × | × | × | × | × | × | × | +| MongoDB | √ | √ | √ | × | × | × | × | √ | × | × | | Phoenix | √ | × | √ | × | × | × | × | × | × | × | | ODPS | √ | × | × | × | × | × | × | × | × | × | | ClickHouse | √ | √ | √ | × | × | × | × | × | × | × | diff --git a/sql/engines/__init__.py b/sql/engines/__init__.py index a101abed13..cbdef337d7 100644 --- a/sql/engines/__init__.py +++ b/sql/engines/__init__.py @@ -132,6 +132,26 @@ def get_tables_metas_data(self, db_name, **kwargs): """获取数据库所有表格信息,用作数据字典导出接口""" return list() + def get_all_databases_summary(self): + """实例数据库管理功能,获取实例所有的数据库描述信息""" + return ResultSet() + + def get_instance_users_summary(self): + """实例账号管理功能,获取实例所有账号信息""" + return ResultSet() + + def create_instance_user(self, **kwargs): + """实例账号管理功能,创建实例账号""" + return ResultSet() + + def drop_instance_user(self, **kwargs): + """实例账号管理功能,删除实例账号""" + return ResultSet() + + def reset_instance_user_pwd(self, **kwargs): + """实例账号管理功能,重置实例账号密码""" + return ResultSet() + def get_all_columns_by_tb(self, db_name, tb_name, **kwargs): """获取所有字段, 返回一个ResultSet,rows=list""" return ResultSet() diff --git a/sql/engines/mongo.py b/sql/engines/mongo.py index 7a0f480a42..13d5f90af0 100644 --- a/sql/engines/mongo.py +++ b/sql/engines/mongo.py @@ -1270,3 +1270,103 @@ def kill_op(self, opids): f"mongodb语句执行killOp报错,语句:db.runCommand({sql}) ,错误信息{traceback.format_exc()}" ) result.error = str(e) + + def get_all_databases_summary(self): + """实例数据库管理功能,获取实例所有的数据库描述信息""" + query_result = self.get_all_databases() + if not query_result.error: + dbs = query_result.rows + conn = self.get_connection() + + # 获取数据库用户信息 + rows = [] + for db_name in dbs: + # 执行语句 + listing = conn[db_name].command(command="usersInfo") + grantees = [] + for user_obj in listing["users"]: + grantees.append( + {"user": user_obj["user"], "roles": user_obj["roles"]}.__str__() + ) + row = { + "db_name": db_name, + "grantees": grantees, + "saved": False, + } + rows.append(row) + query_result.rows = rows + return query_result + + def get_instance_users_summary(self): + """实例账号管理功能,获取实例所有账号信息""" + query_result = self.get_all_databases() + if not query_result.error: + dbs = query_result.rows + conn = self.get_connection() + + # 获取数据库用户信息 + rows = [] + for db_name in dbs: + # 执行语句 + listing = conn[db_name].command(command="usersInfo") + for user_obj in listing["users"]: + rows.append( + { + "db_name_user": f"{db_name}.{user_obj['user']}", + "db_name": db_name, + "user": user_obj["user"], + "roles": [role["role"] for role in user_obj["roles"]], + "saved": False, + } + ) + query_result.rows = rows + return query_result + + def create_instance_user(self, **kwargs): + """实例账号管理功能,创建实例账号""" + exec_result = ResultSet() + db_name = kwargs.get("db_name", "") + user = kwargs.get("user", "") + password1 = kwargs.get("password1", "") + remark = kwargs.get("remark", "") + try: + conn = self.get_connection() + conn[db_name].command("createUser", user, pwd=password1, roles=[]) + exec_result.rows = [ + { + "instance": self.instance, + "db_name": db_name, + "user": user, + "password": password1, + "remark": remark, + } + ] + except Exception as e: + exec_result.error = str(e) + return exec_result + + def drop_instance_user(self, db_name_user: str, **kwarg): + """实例账号管理功能,删除实例账号""" + arr = db_name_user.split(".") + db_name = arr[0] + user = arr[1] + exec_result = ResultSet() + try: + conn = self.get_connection() + conn[db_name].command("dropUser", user) + except Exception as e: + exec_result.error = str(e) + return exec_result + + def reset_instance_user_pwd(self, db_name_user: str, reset_pwd: str, **kwargs): + """实例账号管理功能,重置实例账号密码""" + arr = db_name_user.split(".") + db_name = arr[0] + user = arr[1] + exec_result = ResultSet() + try: + conn = self.get_connection() + conn[db_name].command("updateUser", user, pwd=reset_pwd) + except Exception as e: + exec_result.error = str(e) + return exec_result diff --git a/sql/engines/mysql.py b/sql/engines/mysql.py index ee47fd4240..e2bc7430f6 100644 --- a/sql/engines/mysql.py +++ b/sql/engines/mysql.py @@ -282,6 +282,112 @@ def get_tables_metas_data(self, db_name, **kwargs): table_metas.append(_meta) return table_metas + def get_bind_users(self, db_name: str): + sql_get_bind_users = f"""select group_concat(distinct(GRANTEE)),TABLE_SCHEMA + from information_schema.SCHEMA_PRIVILEGES + where TABLE_SCHEMA='{db_name}' + group by TABLE_SCHEMA;""" + return self.query( + "information_schema", sql_get_bind_users, close_conn=False + ).rows + + def get_all_databases_summary(self): + """实例数据库管理功能,获取实例所有的数据库描述信息""" + # 获取所有数据库 + sql_get_db = """SELECT SCHEMA_NAME,DEFAULT_CHARACTER_SET_NAME,DEFAULT_COLLATION_NAME + FROM information_schema.SCHEMATA + WHERE SCHEMA_NAME NOT IN ('information_schema', 'performance_schema', 'mysql', 'test', 'sys');""" + query_result = self.query("information_schema", sql_get_db, close_conn=False) + if not query_result.error: + dbs = query_result.rows + # 获取数据库关联用户信息 + rows = [] + for db in dbs: + bind_users = self.get_bind_users(db_name=db[0]) + row = { + "db_name": db[0], + "charset": db[1], + "collation": db[2], + "grantees": bind_users[0][0].split(",") if bind_users else [], + "saved": False, + } + rows.append(row) + query_result.rows = rows + return query_result + + def get_instance_users_summary(self): + """实例账号管理功能,获取实例所有账号信息""" + server_version = self.server_version + # MySQL 5.7.6版本起支持ACCOUNT LOCK + if server_version >= (5, 7, 6): + sql_get_user = "select concat('`', user, '`', '@', '`', host,'`') as query,user,host,account_locked from mysql.user;" + else: + sql_get_user = "select concat('`', user, '`', '@', '`', host,'`') as query,user,host from mysql.user;" + query_result = self.query("mysql", sql_get_user) + if not query_result.error: + db_users = query_result.rows + # 获取用户权限信息 + rows = [] + for db_user in db_users: + user_host = db_user[0] + user_priv = self.query( + "mysql", "show grants for {};".format(user_host), close_conn=False + ).rows + row = { + "user_host": user_host, + "user": db_user[1], + "host": db_user[2], + "privileges": user_priv, + "saved": False, + "is_locked": db_user[3] if server_version >= (5, 7, 6) else None, + } + rows.append(row) + query_result.rows = rows + return query_result + + def create_instance_user(self, **kwargs): + """实例账号管理功能,创建实例账号""" + # escape + user = MySQLdb.escape_string(kwargs.get("user", "")).decode("utf-8") + host = MySQLdb.escape_string(kwargs.get("host", "")).decode("utf-8") + password1 = MySQLdb.escape_string(kwargs.get("password1", "")).decode("utf-8") + remark = kwargs.get("remark", "") + # 在一个事务内执行 + hosts = host.split("|") + create_user_cmd = "" + accounts = [] + for host in hosts: + create_user_cmd += ( + f"create user '{user}'@'{host}' identified by '{password1}';" + ) + accounts.append( + { + "instance": self.instance, + "user": user, + "host": host, + "password": password1, + "remark": remark, + } + ) + exec_result = self.execute(db_name="mysql", sql=create_user_cmd) + exec_result.rows = accounts + return exec_result + + def drop_instance_user(self, user_host: str, **kwarg): + """实例账号管理功能,删除实例账号""" + # escape + user_host = MySQLdb.escape_string(user_host).decode("utf-8") + return self.execute(db_name="mysql", sql=f"DROP USER {user_host};") + + def reset_instance_user_pwd(self, user_host: str, reset_pwd: str, **kwargs): + """实例账号管理功能,重置实例账号密码""" + # escape + user_host = MySQLdb.escape_string(user_host).decode("utf-8") + reset_pwd = MySQLdb.escape_string(reset_pwd).decode("utf-8") + return self.execute( + db_name="mysql", sql=f"ALTER USER {user_host} IDENTIFIED BY '{reset_pwd}';" + ) + def get_all_columns_by_tb(self, db_name, tb_name, **kwargs): """获取所有字段, 返回一个ResultSet""" sql = f"""SELECT diff --git a/sql/engines/tests.py b/sql/engines/tests.py index 9ae88cabad..0b9e7078d9 100644 --- a/sql/engines/tests.py +++ b/sql/engines/tests.py @@ -661,6 +661,59 @@ def test_get_long_transaction(self, _query): r = new_engine.get_long_transaction() self.assertIsInstance(r, ResultSet) + @patch.object(MysqlEngine, "get_bind_users") + @patch.object(MysqlEngine, "query") + def test_get_all_databases_summary(self, _query, _get_bind_users): + db_result1 = ResultSet() + db_result1.rows = [("some_db", "utf8mb4", "utf8mb4_general_ci")] + _query.return_value = db_result1 + _get_bind_users.return_value = [("'some_user'@'%'", "cooperate_sign")] + new_engine = MysqlEngine(instance=self.ins1) + dbs = new_engine.get_all_databases_summary() + self.assertEqual( + dbs.rows, + [ + { + "db_name": "some_db", + "charset": "utf8mb4", + "collation": "utf8mb4_general_ci", + "grantees": ["'some_user'@'%'"], + "saved": False, + } + ], + ) + + @patch("MySQLdb.connect") + @patch.object(MysqlEngine, "query") + def test_get_instance_users_summary(self, _query, _connect): + result = ResultSet() + result.error = "query error" + _query.return_value = result + new_engine = MysqlEngine(instance=self.ins1) + user_summary = new_engine.get_instance_users_summary() + self.assertEqual(user_summary.error, "query error") + + @patch("MySQLdb.connect") + @patch.object(MysqlEngine, "execute") + def test_create_instance_user(self, _execute, _connect): + _execute.return_value = ResultSet() + new_engine = MysqlEngine(instance=self.ins1) + result = new_engine.create_instance_user( + user="some_user", host="%", password1="123456", remark="" + ) + self.assertEqual( + result.rows, + [ + { + "instance": self.ins1, + "user": "some_user", + "host": "%", + "password": "123456", + "remark": "", + } + ], + ) + class TestRedis(TestCase): @classmethod @@ -2132,6 +2185,88 @@ def command(self, *arg, **kwargs): self.engine.kill_op(["shards: 111", "shards: 222"]) mock_conn.admin.command.assert_called() + @patch("pymongo.database.Database.command") + @patch("sql.engines.mongo.MongoEngine.get_all_databases") + def test_get_all_databases_summary(self, _mock_all_databases, _mock_command): + db_result = ResultSet() + db_result.rows = ["admin"] + _mock_all_databases.return_value = db_result + _mock_command.return_value = { + "users": [ + { + "_id": "admin.root", + "user": "root", + "db": "admin", + "roles": [{"role": "root", "db": "admin"}], + "mechanisms": ["SCRAM-SHA-1", "SCRAM-SHA-256"], + } + ], + "ok": 1.0, + } + database_summary = self.engine.get_all_databases_summary() + self.assertEqual( + database_summary.rows, + [ + { + "db_name": "admin", + "grantees": [ + "{'user': 'root', 'roles': [{'role': 'root', 'db': 'admin'}]}" + ], + "saved": False, + } + ], + ) + + @patch("pymongo.database.Database.command") + @patch("sql.engines.mongo.MongoEngine.get_all_databases") + def test_get_instance_users_summary(self, _mock_all_databases, _mock_command): + db_result = ResultSet() + db_result.rows = ["admin"] + _mock_all_databases.return_value = db_result + _mock_command.return_value = { + "users": [ + { + "_id": "admin.root", + "user": "root", + "db": "admin", + "roles": [{"role": "root", "db": "admin"}], + "mechanisms": ["SCRAM-SHA-1", "SCRAM-SHA-256"], + } + ], + "ok": 1.0, + } + database_summary = self.engine.get_instance_users_summary() + self.assertEqual( + database_summary.rows, + [ + { + "db_name_user": "admin.root", + "db_name": "admin", + "user": "root", + "roles": ["root"], + "saved": False, + } + ], + ) + + @patch("pymongo.database.Database.command") + def test_create_instance_user(self, _mock_command): + result = self.engine.create_instance_user( + db_name="test", user="some_user", password1="123456", remark="" + ) + self.assertEqual( + result.rows, + [ + { + "instance": self.ins, + "db_name": "test", + "user": "some_user", + "password": "123456", + "remark": "", + } + ], + ) + class TestClickHouse(TestCase): def setUp(self): diff --git a/sql/instance_account.py b/sql/instance_account.py index 06b71fe961..be3188ed56 100644 --- a/sql/instance_account.py +++ b/sql/instance_account.py @@ -6,9 +6,13 @@ from django.core.exceptions import ValidationError from django.http import HttpResponse, JsonResponse - +from sql.utils.instance_management import ( + SUPPORTED_MANAGEMENT_DB_TYPE, + get_instanceaccount_unique_value, + get_instanceaccount_unique_key, +) from common.utils.extend_json_encoder import ExtendJSONEncoder -from sql.engines import get_engine +from sql.engines import get_engine, ResultSet from sql.utils.resource_group import user_instances from .models import Instance, InstanceAccount @@ -22,46 +26,29 @@ def users(request): if not instance_id: return JsonResponse({"status": 0, "msg": "", "data": []}) try: - instance = user_instances(request.user, db_type=["mysql"]).get(id=instance_id) + instance = user_instances( + request.user, db_type=SUPPORTED_MANAGEMENT_DB_TYPE + ).get(id=instance_id) except Instance.DoesNotExist: return JsonResponse({"status": 1, "msg": "你所在组未关联该实例", "data": []}) # 获取已录入用户 cnf_users = dict() for user in InstanceAccount.objects.filter(instance=instance).values( - "id", "user", "host", "remark" + "id", "user", "host", "db_name", "remark" ): user["saved"] = True - cnf_users[f"`{user['user']}`@`{user['host']}`"] = user + cnf_users[get_instanceaccount_unique_value(instance.db_type, user)] = user # 获取所有用户 query_engine = get_engine(instance=instance) - server_version = query_engine.server_version - # MySQL 5.7.6版本起支持ACCOUNT LOCK - if server_version >= (5, 7, 6): - sql_get_user = "select concat('`', user, '`', '@', '`', host,'`') as query,user,host,account_locked from mysql.user;" - else: - sql_get_user = "select concat('`', user, '`', '@', '`', host,'`') as query,user,host from mysql.user;" - query_result = query_engine.query("mysql", sql_get_user) + query_result = query_engine.get_instance_users_summary() if not query_result.error: - db_users = query_result.rows - # 获取用户权限信息 rows = [] - for db_user in db_users: - user_host = db_user[0] - user_priv = query_engine.query( - "mysql", "show grants for {};".format(user_host), close_conn=False - ).rows - row = { - "user_host": user_host, - "user": db_user[1], - "host": db_user[2], - "privileges": user_priv, - "saved": False, - "is_locked": db_user[3] if server_version >= (5, 7, 6) else None, - } + key = get_instanceaccount_unique_key(db_type=instance.db_type) + for row in query_result.rows: # 合并数据 - if user_host in cnf_users.keys(): - row = dict(row, **cnf_users[user_host]) + if row[key] in cnf_users.keys(): + row = dict(row, **cnf_users[row[key]]) rows.append(row) # 过滤参数 if saved: @@ -83,6 +70,7 @@ def users(request): def create(request): """创建数据库账号""" instance_id = request.POST.get("instance_id", 0) + db_name = request.POST.get("db_name") user = request.POST.get("user") host = request.POST.get("host") password1 = request.POST.get("password1") @@ -90,11 +78,17 @@ def create(request): remark = request.POST.get("remark", "") try: - instance = user_instances(request.user, db_type=["mysql"]).get(id=instance_id) + instance = user_instances( + request.user, db_type=SUPPORTED_MANAGEMENT_DB_TYPE + ).get(id=instance_id) except Instance.DoesNotExist: return JsonResponse({"status": 1, "msg": "你所在组未关联该实例", "data": []}) - if not all([user, host, password1, password2]): + if ( + instance.db_type == "mysql" and not all([user, host, password1, password2]) + ) or ( + instance.db_type == "mongo" and not all([db_name, user, password1, password2]) + ): return JsonResponse({"status": 1, "msg": "参数不完整,请确认后提交", "data": []}) if password1 != password2: @@ -106,34 +100,20 @@ def create(request): except ValidationError as msg: return JsonResponse({"status": 1, "msg": f"{msg}", "data": []}) - # escape - user = MySQLdb.escape_string(user).decode("utf-8") - host = MySQLdb.escape_string(host).decode("utf-8") - password1 = MySQLdb.escape_string(password1).decode("utf-8") - engine = get_engine(instance=instance) - # 在一个事务内执行 - hosts = host.split("|") - create_user_cmd = "" - accounts = [] - for host in hosts: - create_user_cmd += f"create user '{user}'@'{host}' identified by '{password1}';" - accounts.append( - InstanceAccount( - instance=instance, - user=user, - host=host, - password=password1, - remark=remark, - ) - ) - exec_result = engine.execute(db_name="mysql", sql=create_user_cmd) + exec_result = engine.create_instance_user( + db_name=db_name, user=user, host=host, password1=password1, remark=remark + ) + # 关闭连接 + engine.close() if exec_result.error: return JsonResponse({"status": 1, "msg": exec_result.error}) # 保存到数据库 else: + accounts = [InstanceAccount(**row) for row in exec_result.rows] InstanceAccount.objects.bulk_create(accounts) + return JsonResponse({"status": 0, "msg": "", "data": []}) @@ -141,17 +121,22 @@ def create(request): def edit(request): """修改、录入数据库账号""" instance_id = request.POST.get("instance_id", 0) + db_name = request.POST.get("db_name", "") user = request.POST.get("user") - host = request.POST.get("host") + host = request.POST.get("host", "") password = request.POST.get("password") remark = request.POST.get("remark", "") try: - instance = user_instances(request.user, db_type=["mysql"]).get(id=instance_id) + instance = user_instances( + request.user, db_type=SUPPORTED_MANAGEMENT_DB_TYPE + ).get(id=instance_id) except Instance.DoesNotExist: return JsonResponse({"status": 1, "msg": "你所在组未关联该实例", "data": []}) - if not all([user, host]): + if (instance.db_type == "mysql" and not all([user, host])) or ( + instance.db_type == "mongo" and not all([db_name, user]) + ): return JsonResponse({"status": 1, "msg": "参数不完整,请确认后提交", "data": []}) # 保存到数据库 @@ -160,11 +145,16 @@ def edit(request): instance=instance, user=user, host=host, + db_name=db_name, defaults={"password": password, "remark": remark}, ) else: InstanceAccount.objects.update_or_create( - instance=instance, user=user, host=host, defaults={"remark": remark} + instance=instance, + user=user, + host=host, + db_name=db_name, + defaults={"remark": remark}, ) return JsonResponse({"status": 0, "msg": "", "data": []}) @@ -173,77 +163,95 @@ def edit(request): def grant(request): """获取用户权限变更语句,并执行权限变更""" instance_id = request.POST.get("instance_id", 0) - user_host = request.POST.get("user_host") - op_type = int(request.POST.get("op_type")) - priv_type = int(request.POST.get("priv_type")) - privs = json.loads(request.POST.get("privs")) grant_sql = "" - # escape - user_host = MySQLdb.escape_string(user_host).decode("utf-8") - - # 全局权限 - if priv_type == 0: - global_privs = privs["global_privs"] - if not all([global_privs]): - return JsonResponse({"status": 1, "msg": "信息不完整,请确认后提交", "data": []}) - global_privs = ["GRANT OPTION" if g == "GRANT" else g for g in global_privs] - if op_type == 0: - grant_sql = f"GRANT {','.join(global_privs)} ON *.* TO {user_host};" - elif op_type == 1: - grant_sql = f"REVOKE {','.join(global_privs)} ON *.* FROM {user_host};" - - # 库权限 - elif priv_type == 1: - db_privs = privs["db_privs"] - db_name = request.POST.getlist("db_name[]") - if not all([db_privs, db_name]): - return JsonResponse({"status": 1, "msg": "信息不完整,请确认后提交", "data": []}) - for db in db_name: - db_privs = ["GRANT OPTION" if d == "GRANT" else d for d in db_privs] - if op_type == 0: - grant_sql += f"GRANT {','.join(db_privs)} ON `{db}`.* TO {user_host};" - elif op_type == 1: - grant_sql += ( - f"REVOKE {','.join(db_privs)} ON `{db}`.* FROM {user_host};" - ) - # 表权限 - elif priv_type == 2: - tb_privs = privs["tb_privs"] - db_name = request.POST.get("db_name") - tb_name = request.POST.getlist("tb_name[]") - if not all([tb_privs, db_name, tb_name]): - return JsonResponse({"status": 1, "msg": "信息不完整,请确认后提交", "data": []}) - for tb in tb_name: - tb_privs = ["GRANT OPTION" if t == "GRANT" else t for t in tb_privs] - if op_type == 0: - grant_sql += ( - f"GRANT {','.join(tb_privs)} ON `{db_name}`.`{tb}` TO {user_host};" - ) - elif op_type == 1: - grant_sql += f"REVOKE {','.join(tb_privs)} ON `{db_name}`.`{tb}` FROM {user_host};" - # 列权限 - elif priv_type == 3: - col_privs = privs["col_privs"] - db_name = request.POST.get("db_name") - tb_name = request.POST.get("tb_name") - col_name = request.POST.getlist("col_name[]") - if not all([col_privs, db_name, tb_name, col_name]): - return JsonResponse({"status": 1, "msg": "信息不完整,请确认后提交", "data": []}) - for priv in col_privs: - if op_type == 0: - grant_sql += f"GRANT {priv}(`{'`,`'.join(col_name)}`) ON `{db_name}`.`{tb_name}` TO {user_host};" - elif op_type == 1: - grant_sql += f"REVOKE {priv}(`{'`,`'.join(col_name)}`) ON `{db_name}`.`{tb_name}` FROM {user_host};" - - # 执行变更语句 try: - instance = user_instances(request.user, db_type=["mysql"]).get(id=instance_id) + instance = user_instances( + request.user, db_type=SUPPORTED_MANAGEMENT_DB_TYPE + ).get(id=instance_id) except Instance.DoesNotExist: return JsonResponse({"status": 1, "msg": "你所在组未关联该实例", "data": []}) engine = get_engine(instance=instance) - exec_result = engine.execute(db_name="mysql", sql=grant_sql) + if instance.db_type == "mysql": + user_host = request.POST.get("user_host") + op_type = int(request.POST.get("op_type")) + priv_type = int(request.POST.get("priv_type")) + privs = json.loads(request.POST.get("privs")) + + # escape + user_host = MySQLdb.escape_string(user_host).decode("utf-8") + + # 全局权限 + if priv_type == 0: + global_privs = privs["global_privs"] + if not all([global_privs]): + return JsonResponse({"status": 1, "msg": "信息不完整,请确认后提交", "data": []}) + global_privs = ["GRANT OPTION" if g == "GRANT" else g for g in global_privs] + if op_type == 0: + grant_sql = f"GRANT {','.join(global_privs)} ON *.* TO {user_host};" + elif op_type == 1: + grant_sql = f"REVOKE {','.join(global_privs)} ON *.* FROM {user_host};" + + # 库权限 + elif priv_type == 1: + db_privs = privs["db_privs"] + db_name = request.POST.getlist("db_name[]") + if not all([db_privs, db_name]): + return JsonResponse({"status": 1, "msg": "信息不完整,请确认后提交", "data": []}) + for db in db_name: + db_privs = ["GRANT OPTION" if d == "GRANT" else d for d in db_privs] + if op_type == 0: + grant_sql += ( + f"GRANT {','.join(db_privs)} ON `{db}`.* TO {user_host};" + ) + elif op_type == 1: + grant_sql += ( + f"REVOKE {','.join(db_privs)} ON `{db}`.* FROM {user_host};" + ) + # 表权限 + elif priv_type == 2: + tb_privs = privs["tb_privs"] + db_name = request.POST.get("db_name") + tb_name = request.POST.getlist("tb_name[]") + if not all([tb_privs, db_name, tb_name]): + return JsonResponse({"status": 1, "msg": "信息不完整,请确认后提交", "data": []}) + for tb in tb_name: + tb_privs = ["GRANT OPTION" if t == "GRANT" else t for t in tb_privs] + if op_type == 0: + grant_sql += f"GRANT {','.join(tb_privs)} ON `{db_name}`.`{tb}` TO {user_host};" + elif op_type == 1: + grant_sql += f"REVOKE {','.join(tb_privs)} ON `{db_name}`.`{tb}` FROM {user_host};" + # 列权限 + elif priv_type == 3: + col_privs = privs["col_privs"] + db_name = request.POST.get("db_name") + tb_name = request.POST.get("tb_name") + col_name = request.POST.getlist("col_name[]") + if not all([col_privs, db_name, tb_name, col_name]): + return JsonResponse({"status": 1, "msg": "信息不完整,请确认后提交", "data": []}) + for priv in col_privs: + if op_type == 0: + grant_sql += f"GRANT {priv}(`{'`,`'.join(col_name)}`) ON `{db_name}`.`{tb_name}` TO {user_host};" + elif op_type == 1: + grant_sql += f"REVOKE {priv}(`{'`,`'.join(col_name)}`) ON `{db_name}`.`{tb_name}` FROM {user_host};" + # 执行变更语句 + exec_result = engine.execute(db_name="mysql", sql=grant_sql) + elif instance.db_type == "mongo": + db_name_user = request.POST.get("db_name_user") + roles = request.POST.getlist("roles[]") + arr = db_name_user.split(".") + db_name = arr[0] + user = arr[1] + exec_result = ResultSet() + try: + conn = engine.get_connection() + conn[db_name].command("updateUser", user, roles=roles) + except Exception as e: + exec_result.error = str(e) + + # 关闭连接 + engine.close() if exec_result.error: return JsonResponse({"status": 1, "msg": exec_result.error}) return JsonResponse({"status": 0, "msg": "", "data": grant_sql}) @@ -253,26 +261,30 @@ def grant(request): def reset_pwd(request): """创建数据库账号""" instance_id = request.POST.get("instance_id", 0) + db_name_user = request.POST.get("db_name_user") + db_name = request.POST.get("db_name", "") user_host = request.POST.get("user_host") user = request.POST.get("user") - host = request.POST.get("host") + host = request.POST.get("host", "") reset_pwd1 = request.POST.get("reset_pwd1") reset_pwd2 = request.POST.get("reset_pwd2") - if not all([user, host, reset_pwd1, reset_pwd2]): - return JsonResponse({"status": 1, "msg": "参数不完整,请确认后提交", "data": []}) - - if reset_pwd1 != reset_pwd2: - return JsonResponse({"status": 1, "msg": "两次输入密码不一致", "data": []}) - try: - instance = user_instances(request.user, db_type=["mysql"]).get(id=instance_id) + instance = user_instances( + request.user, db_type=SUPPORTED_MANAGEMENT_DB_TYPE + ).get(id=instance_id) except Instance.DoesNotExist: return JsonResponse({"status": 1, "msg": "你所在组未关联该实例", "data": []}) - # escape - user_host = MySQLdb.escape_string(user_host).decode("utf-8") - reset_pwd1 = MySQLdb.escape_string(reset_pwd1).decode("utf-8") + if ( + instance.db_type == "mysql" and not all([user, host, reset_pwd1, reset_pwd2]) + ) or ( + instance.db_type == "mongo" and not all([db_name, user, reset_pwd1, reset_pwd2]) + ): + return JsonResponse({"status": 1, "msg": "参数不完整,请确认后提交", "data": []}) + + if reset_pwd1 != reset_pwd2: + return JsonResponse({"status": 1, "msg": "两次输入密码不一致", "data": []}) # TODO 目前使用系统自带验证,后续实现验证器校验 try: @@ -281,17 +293,24 @@ def reset_pwd(request): return JsonResponse({"status": 1, "msg": f"{msg}", "data": []}) engine = get_engine(instance=instance) - exec_result = engine.execute( - db_name="mysql", sql=f"ALTER USER {user_host} IDENTIFIED BY '{reset_pwd1}';" + exec_result = engine.reset_instance_user_pwd( + user_host=user_host, db_name_user=db_name_user, reset_pwd=reset_pwd1 ) + # 关闭连接 + engine.close() if exec_result.error: result = {"status": 1, "msg": exec_result.error} return HttpResponse(json.dumps(result), content_type="application/json") # 保存到数据库 else: InstanceAccount.objects.update_or_create( - instance=instance, user=user, host=host, defaults={"password": reset_pwd1} + instance=instance, + user=user, + host=host, + db_name=db_name, + defaults={"password": reset_pwd1}, ) + return JsonResponse({"status": 0, "msg": "", "data": []}) @@ -330,26 +349,36 @@ def lock(request): def delete(request): """删除账号""" instance_id = request.POST.get("instance_id", 0) + db_name_user = request.POST.get("db_name_user") + db_name = request.POST.get("db_name") user_host = request.POST.get("user_host") user = request.POST.get("user") host = request.POST.get("host") - if not all([user_host]): - return JsonResponse({"status": 1, "msg": "参数不完整,请确认后提交", "data": []}) - try: - instance = user_instances(request.user, db_type=["mysql"]).get(id=instance_id) + instance = user_instances( + request.user, db_type=SUPPORTED_MANAGEMENT_DB_TYPE + ).get(id=instance_id) except Instance.DoesNotExist: return JsonResponse({"status": 1, "msg": "你所在组未关联该实例", "data": []}) - # escape - user_host = MySQLdb.escape_string(user_host).decode("utf-8") + if (instance.db_type == "mysql" and not all([user_host])) or ( + instance.db_type == "mongo" and not all([db_name_user]) + ): + return JsonResponse({"status": 1, "msg": "参数不完整,请确认后提交", "data": []}) engine = get_engine(instance=instance) - exec_result = engine.execute(db_name="mysql", sql=f"DROP USER {user_host};") + exec_result = engine.drop_instance_user( + user_host=user_host, db_name_user=db_name_user + ) + # 关闭连接 + engine.close() if exec_result.error: return JsonResponse({"status": 1, "msg": exec_result.error}) # 删除数据库对应记录 else: - InstanceAccount.objects.filter(instance=instance, user=user, host=host).delete() + InstanceAccount.objects.filter( + instance=instance, user=user, host=host, db_name=db_name + ).delete() + return JsonResponse({"status": 0, "msg": "", "data": []}) diff --git a/sql/instance_database.py b/sql/instance_database.py index 15d1572c35..61418d7e15 100644 --- a/sql/instance_database.py +++ b/sql/instance_database.py @@ -13,7 +13,7 @@ from django_redis import get_redis_connection from common.utils.extend_json_encoder import ExtendJSONEncoder -from sql.engines import get_engine +from sql.engines import get_engine, ResultSet from sql.models import Instance, InstanceDatabase, Users from sql.utils.resource_group import user_instances @@ -30,7 +30,9 @@ def databases(request): return JsonResponse({"status": 0, "msg": "", "data": []}) try: - instance = user_instances(request.user, db_type=["mysql"]).get(id=instance_id) + instance = user_instances(request.user, db_type=["mysql", "mongo"]).get( + id=instance_id + ) except Instance.DoesNotExist: return JsonResponse({"status": 1, "msg": "你所在组未关联该实例", "data": []}) @@ -42,42 +44,17 @@ def databases(request): db["saved"] = True cnf_dbs[f"{db['db_name']}"] = db - # 获取所有数据库 - sql_get_db = """SELECT SCHEMA_NAME,DEFAULT_CHARACTER_SET_NAME,DEFAULT_COLLATION_NAME -FROM information_schema.SCHEMATA -WHERE SCHEMA_NAME NOT IN ('information_schema', 'performance_schema', 'mysql', 'test', 'sys');""" query_engine = get_engine(instance=instance) - query_result = query_engine.query( - "information_schema", sql_get_db, close_conn=False - ) + query_result = query_engine.get_all_databases_summary() if not query_result.error: - dbs = query_result.rows # 获取数据库关联用户信息 rows = [] - for db in dbs: - db_name = db[0] - sql_get_bind_users = f"""select group_concat(distinct(GRANTEE)),TABLE_SCHEMA -from information_schema.SCHEMA_PRIVILEGES -where TABLE_SCHEMA='{db_name}' -group by TABLE_SCHEMA;""" - bind_users = query_engine.query( - "information_schema", sql_get_bind_users, close_conn=False - ).rows - row = { - "db_name": db_name, - "charset": db[1], - "collation": db[2], - "grantees": bind_users[0][0].split(",") if bind_users else [], - "saved": False, - } - # 合并数据 - if db_name in cnf_dbs.keys(): - row = dict(row, **cnf_dbs[db_name]) + for row in query_result.rows: + if row["db_name"] in cnf_dbs.keys(): + row = dict(row, **cnf_dbs[row["db_name"]]) rows.append(row) - # 过滤参数 if saved: rows = [row for row in rows if row["saved"]] - result = {"status": 0, "msg": "ok", "rows": rows} else: result = {"status": 1, "msg": query_result.error} @@ -102,7 +79,9 @@ def create(request): return JsonResponse({"status": 1, "msg": "参数不完整,请确认后提交", "data": []}) try: - instance = user_instances(request.user, db_type=["mysql"]).get(id=instance_id) + instance = user_instances(request.user, db_type=["mysql", "mongo"]).get( + id=instance_id + ) except Instance.DoesNotExist: return JsonResponse({"status": 1, "msg": "你所在组未关联该实例", "data": []}) @@ -111,13 +90,26 @@ def create(request): except Users.DoesNotExist: return JsonResponse({"status": 1, "msg": "负责人不存在", "data": []}) - # escape - db_name = MySQLdb.escape_string(db_name).decode("utf-8") - engine = get_engine(instance=instance) - exec_result = engine.execute( - db_name="information_schema", sql=f"create database {db_name};" - ) + if instance.db_type == "mysql": + # escape + db_name = MySQLdb.escape_string(db_name).decode("utf-8") + exec_result = engine.execute( + db_name="information_schema", sql=f"create database {db_name};" + ) + elif instance.db_type == "mongo": + exec_result = ResultSet() + try: + conn = engine.get_connection() + db = conn[db_name] + db.create_collection( + name=f"archery-{db_name}" + ) # mongo创建数据库,需要数据库存在数据才会显示数据库名称,这里创建一个archery-{db_name}的集合 + except Exception as e: + exec_result.error = f"创建数据库失败, 错误信息:{str(e)}" + + # 关闭连接 + engine.close() if exec_result.error: return JsonResponse({"status": 1, "msg": exec_result.error}) # 保存到数据库 @@ -133,6 +125,7 @@ def create(request): r = get_redis_connection("default") for key in r.scan_iter(match="*insRes*", count=2000): r.delete(key) + return JsonResponse({"status": 0, "msg": "", "data": []}) @@ -148,7 +141,9 @@ def edit(request): return JsonResponse({"status": 1, "msg": "参数不完整,请确认后提交", "data": []}) try: - instance = user_instances(request.user, db_type=["mysql"]).get(id=instance_id) + instance = user_instances(request.user, db_type=["mysql", "mongo"]).get( + id=instance_id + ) except Instance.DoesNotExist: return JsonResponse({"status": 1, "msg": "你所在组未关联该实例", "data": []}) diff --git a/sql/models.py b/sql/models.py index 4cc7b316db..5043b5704f 100755 --- a/sql/models.py +++ b/sql/models.py @@ -605,7 +605,8 @@ class InstanceAccount(models.Model): instance = models.ForeignKey(Instance, on_delete=models.CASCADE) user = fields.EncryptedCharField(verbose_name="账号", max_length=128) - host = models.CharField(verbose_name="主机", max_length=64) + host = models.CharField(verbose_name="主机", max_length=64) # mysql数据库存储主机信息 + db_name = models.CharField(verbose_name="数据库名称", max_length=128) # mongo数据库存储数据库名称 password = fields.EncryptedCharField( verbose_name="密码", max_length=128, default="", blank=True ) diff --git a/sql/templates/database.html b/sql/templates/database.html index 995a694ad6..e60bccbef6 100755 --- a/sql/templates/database.html +++ b/sql/templates/database.html @@ -7,6 +7,8 @@