Initial Codes
This commit is contained in:
@@ -0,0 +1,631 @@
|
||||
"""
|
||||
User & Privilege Management panel.
|
||||
|
||||
Supports MySQL and PostgreSQL.
|
||||
SQLite and MSSQL show an informational message (no user management via this UI).
|
||||
|
||||
Features
|
||||
--------
|
||||
• List all database users with host (MySQL) or connection limit (PostgreSQL)
|
||||
• Create user (username + password + optional host for MySQL)
|
||||
• Drop user with confirmation
|
||||
• View per-database GRANT privileges for the selected user
|
||||
• GRANT / REVOKE individual privileges on a database
|
||||
|
||||
All DDL operations execute synchronously in a background QThread to keep
|
||||
the UI responsive.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QSplitter,
|
||||
QTableWidget, QTableWidgetItem, QHeaderView,
|
||||
QLabel, QPushButton, QComboBox, QCheckBox,
|
||||
QDialog, QFormLayout, QLineEdit, QDialogButtonBox,
|
||||
QMessageBox, QGroupBox, QAbstractItemView,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QColor, QFont
|
||||
|
||||
|
||||
# ── Standard SQL privileges offered in the GRANT UI ──────────────────────────
|
||||
|
||||
_MYSQL_PRIVS = [
|
||||
"SELECT", "INSERT", "UPDATE", "DELETE",
|
||||
"CREATE", "DROP", "ALTER", "INDEX",
|
||||
"CREATE VIEW", "SHOW VIEW",
|
||||
"CREATE ROUTINE", "ALTER ROUTINE", "EXECUTE",
|
||||
"REFERENCES", "TRIGGER", "LOCK TABLES",
|
||||
"CREATE TEMPORARY TABLES",
|
||||
]
|
||||
|
||||
_PG_PRIVS = [
|
||||
"SELECT", "INSERT", "UPDATE", "DELETE",
|
||||
"TRUNCATE", "REFERENCES", "TRIGGER",
|
||||
"CREATE", "CONNECT", "TEMPORARY",
|
||||
"EXECUTE", "USAGE",
|
||||
]
|
||||
|
||||
|
||||
# ── Background worker ─────────────────────────────────────────────────────────
|
||||
|
||||
class _UserWorker(QThread):
|
||||
"""Run an arbitrary callable in a background thread."""
|
||||
result = pyqtSignal(object)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, fn, *args, parent=None):
|
||||
super().__init__(parent)
|
||||
self._fn = fn
|
||||
self._args = args
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.result.emit(self._fn(*self._args))
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
# ── Driver-level helpers (attached at runtime, no driver subclassing needed) ──
|
||||
|
||||
def _mysql_get_users(driver) -> list[dict]:
|
||||
"""Return list of {user, host, super} from MySQL.user table."""
|
||||
cols, rows, _ = driver.execute_query(
|
||||
"SELECT User, Host, Super_priv FROM mysql.user ORDER BY User, Host"
|
||||
)
|
||||
return [{"user": r[0], "host": r[1], "super": r[2]} for r in rows]
|
||||
|
||||
|
||||
def _mysql_get_grants(driver, user: str, host: str) -> list[str]:
|
||||
cols, rows, _ = driver.execute_query(
|
||||
f"SHOW GRANTS FOR %s@%s", (user, host) # type: ignore[arg-type]
|
||||
)
|
||||
return [r[0] for r in rows]
|
||||
|
||||
|
||||
def _mysql_create_user(driver, user: str, host: str, password: str):
|
||||
driver.execute_query(
|
||||
"CREATE USER %s@%s IDENTIFIED BY %s", (user, host, password)
|
||||
)
|
||||
|
||||
|
||||
def _mysql_drop_user(driver, user: str, host: str):
|
||||
driver.execute_query(f"DROP USER %s@%s", (user, host))
|
||||
|
||||
|
||||
def _mysql_grant(driver, privs: list[str], database: str,
|
||||
user: str, host: str):
|
||||
priv_str = ", ".join(privs) if privs else "USAGE"
|
||||
driver.execute_query(
|
||||
f"GRANT {priv_str} ON `{database}`.* TO %s@%s", (user, host)
|
||||
)
|
||||
driver.execute_query("FLUSH PRIVILEGES")
|
||||
|
||||
|
||||
def _mysql_revoke(driver, privs: list[str], database: str,
|
||||
user: str, host: str):
|
||||
priv_str = ", ".join(privs) if privs else "USAGE"
|
||||
driver.execute_query(
|
||||
f"REVOKE {priv_str} ON `{database}`.* FROM %s@%s", (user, host)
|
||||
)
|
||||
driver.execute_query("FLUSH PRIVILEGES")
|
||||
|
||||
|
||||
# ── PostgreSQL helpers ────────────────────────────────────────────────────────
|
||||
|
||||
def _pg_get_users(driver) -> list[dict]:
|
||||
cols, rows, _ = driver.execute_query(
|
||||
"SELECT usename, usesuper, usecreatedb FROM pg_user ORDER BY usename"
|
||||
)
|
||||
return [{"user": r[0], "host": "", "super": "Y" if r[1] else "N",
|
||||
"createdb": "Y" if r[2] else "N"} for r in rows]
|
||||
|
||||
|
||||
def _pg_get_grants(driver, user: str, _host: str) -> list[str]:
|
||||
cols, rows, _ = driver.execute_query("""
|
||||
SELECT grantee, table_catalog, table_schema, table_name,
|
||||
string_agg(privilege_type, ', ') AS privileges
|
||||
FROM information_schema.role_table_grants
|
||||
WHERE grantee = %s
|
||||
GROUP BY grantee, table_catalog, table_schema, table_name
|
||||
ORDER BY table_catalog, table_schema, table_name
|
||||
""", (user,))
|
||||
return [
|
||||
f"GRANT {r[4]} ON {r[1]}.{r[2]}.{r[3]} TO {r[0]}"
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def _pg_create_user(driver, user: str, _host: str, password: str):
|
||||
driver.execute_query(
|
||||
f"CREATE USER \"{user}\" WITH PASSWORD %s", (password,)
|
||||
)
|
||||
|
||||
|
||||
def _pg_drop_user(driver, user: str, _host: str):
|
||||
driver.execute_query(f'DROP USER "{user}"')
|
||||
|
||||
|
||||
def _pg_grant(driver, privs: list[str], database: str, user: str, _host: str):
|
||||
priv_str = ", ".join(privs) if privs else "CONNECT"
|
||||
driver.execute_query(
|
||||
f'GRANT {priv_str} ON DATABASE "{database}" TO "{user}"'
|
||||
)
|
||||
|
||||
|
||||
def _pg_revoke(driver, privs: list[str], database: str, user: str, _host: str):
|
||||
priv_str = ", ".join(privs) if privs else "CONNECT"
|
||||
driver.execute_query(
|
||||
f'REVOKE {priv_str} ON DATABASE "{database}" FROM "{user}"'
|
||||
)
|
||||
|
||||
|
||||
# ── Dialogs ───────────────────────────────────────────────────────────────────
|
||||
|
||||
class _CreateUserDialog(QDialog):
|
||||
def __init__(self, db_type: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Create User")
|
||||
self.setModal(True)
|
||||
self.setMinimumWidth(360)
|
||||
self._db_type = db_type
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
form = QFormLayout()
|
||||
form.setSpacing(8)
|
||||
|
||||
self._user_edit = QLineEdit()
|
||||
self._user_edit.setPlaceholderText("e.g. app_user")
|
||||
form.addRow("Username:", self._user_edit)
|
||||
|
||||
if db_type == "mysql":
|
||||
self._host_edit = QLineEdit()
|
||||
self._host_edit.setText("%")
|
||||
self._host_edit.setPlaceholderText("% = any host")
|
||||
form.addRow("Host:", self._host_edit)
|
||||
|
||||
self._pass_edit = QLineEdit()
|
||||
self._pass_edit.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
self._pass_edit.setPlaceholderText("Password")
|
||||
form.addRow("Password:", self._pass_edit)
|
||||
|
||||
self._pass2_edit = QLineEdit()
|
||||
self._pass2_edit.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
self._pass2_edit.setPlaceholderText("Confirm password")
|
||||
form.addRow("Confirm:", self._pass2_edit)
|
||||
|
||||
root.addLayout(form)
|
||||
|
||||
bbox = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok |
|
||||
QDialogButtonBox.StandardButton.Cancel,
|
||||
)
|
||||
bbox.accepted.connect(self._validate)
|
||||
bbox.rejected.connect(self.reject)
|
||||
root.addWidget(bbox)
|
||||
|
||||
def _validate(self):
|
||||
if not self._user_edit.text().strip():
|
||||
QMessageBox.warning(self, "Validation", "Username is required.")
|
||||
return
|
||||
if self._pass_edit.text() != self._pass2_edit.text():
|
||||
QMessageBox.warning(self, "Validation", "Passwords do not match.")
|
||||
return
|
||||
self.accept()
|
||||
|
||||
@property
|
||||
def username(self) -> str:
|
||||
return self._user_edit.text().strip()
|
||||
|
||||
@property
|
||||
def host(self) -> str:
|
||||
if self._db_type == "mysql":
|
||||
return self._host_edit.text().strip() or "%"
|
||||
return ""
|
||||
|
||||
@property
|
||||
def password(self) -> str:
|
||||
return self._pass_edit.text()
|
||||
|
||||
|
||||
class _GrantDialog(QDialog):
|
||||
"""Select privileges + target database and grant/revoke."""
|
||||
|
||||
def __init__(self, driver, db_type: str, databases: list[str],
|
||||
user: str, host: str, mode: str = "grant", parent=None):
|
||||
super().__init__(parent)
|
||||
self._driver = driver
|
||||
self._db_type = db_type
|
||||
self._user = user
|
||||
self._host = host
|
||||
self._mode = mode # "grant" | "revoke"
|
||||
self.setWindowTitle(f"{'Grant to' if mode == 'grant' else 'Revoke from'} {user}")
|
||||
self.setModal(True)
|
||||
self.setMinimumSize(420, 480)
|
||||
self._build_ui(databases)
|
||||
|
||||
def _build_ui(self, databases: list[str]):
|
||||
root = QVBoxLayout(self)
|
||||
|
||||
form = QFormLayout()
|
||||
self._db_combo = QComboBox()
|
||||
for db in databases:
|
||||
self._db_combo.addItem(db)
|
||||
form.addRow("Database:", self._db_combo)
|
||||
root.addLayout(form)
|
||||
|
||||
# Privilege checkboxes
|
||||
priv_box = QGroupBox("Privileges")
|
||||
pb_lay = QVBoxLayout(priv_box)
|
||||
privs = _MYSQL_PRIVS if self._db_type == "mysql" else _PG_PRIVS
|
||||
|
||||
self._priv_checks: list[QCheckBox] = []
|
||||
for priv in privs:
|
||||
cb = QCheckBox(priv)
|
||||
pb_lay.addWidget(cb)
|
||||
self._priv_checks.append(cb)
|
||||
|
||||
sel_row = QHBoxLayout()
|
||||
sel_all = QPushButton("All")
|
||||
sel_all.setFixedWidth(50)
|
||||
sel_all.clicked.connect(lambda: [cb.setChecked(True)
|
||||
for cb in self._priv_checks])
|
||||
sel_none = QPushButton("None")
|
||||
sel_none.setFixedWidth(50)
|
||||
sel_none.clicked.connect(lambda: [cb.setChecked(False)
|
||||
for cb in self._priv_checks])
|
||||
sel_row.addWidget(sel_all)
|
||||
sel_row.addWidget(sel_none)
|
||||
sel_row.addStretch()
|
||||
pb_lay.insertLayout(0, sel_row)
|
||||
|
||||
root.addWidget(priv_box, 1)
|
||||
|
||||
verb = "Grant" if self._mode == "grant" else "Revoke"
|
||||
bbox = QDialogButtonBox()
|
||||
ok = bbox.addButton(verb, QDialogButtonBox.ButtonRole.AcceptRole)
|
||||
ok.clicked.connect(self._execute)
|
||||
bbox.addButton(QDialogButtonBox.StandardButton.Cancel).clicked.connect(
|
||||
self.reject)
|
||||
root.addWidget(bbox)
|
||||
|
||||
def _execute(self):
|
||||
privs = [cb.text() for cb in self._priv_checks if cb.isChecked()]
|
||||
if not privs:
|
||||
QMessageBox.warning(self, "No Privilege",
|
||||
"Select at least one privilege.")
|
||||
return
|
||||
db = self._db_combo.currentText()
|
||||
|
||||
try:
|
||||
if self._db_type == "mysql":
|
||||
fn = _mysql_grant if self._mode == "grant" else _mysql_revoke
|
||||
else:
|
||||
fn = _pg_grant if self._mode == "grant" else _pg_revoke
|
||||
fn(self._driver, privs, db, self._user, self._host)
|
||||
self.accept()
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", str(e))
|
||||
|
||||
@property
|
||||
def selected_privs(self) -> list[str]:
|
||||
return [cb.text() for cb in self._priv_checks if cb.isChecked()]
|
||||
|
||||
|
||||
# ── Main Panel ────────────────────────────────────────────────────────────────
|
||||
|
||||
class UserManagerPanel(QWidget):
|
||||
"""
|
||||
User & privilege management workspace tab.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
driver : BaseDriver — must already be connected
|
||||
parent : QWidget
|
||||
"""
|
||||
status_message = pyqtSignal(str)
|
||||
|
||||
def __init__(self, driver, parent=None):
|
||||
super().__init__(parent)
|
||||
self._driver = driver
|
||||
self._db_type = getattr(driver, "db_type", "").lower()
|
||||
self._databases: list[str] = []
|
||||
self._current_user: str = ""
|
||||
self._current_host: str = ""
|
||||
self._worker: _UserWorker | None = None
|
||||
self._build_ui()
|
||||
self._load_users()
|
||||
self._load_databases()
|
||||
|
||||
# ── UI construction ───────────────────────────────────────────────────────
|
||||
|
||||
def _build_ui(self):
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(0, 0, 0, 0)
|
||||
root.setSpacing(0)
|
||||
|
||||
# ── Header bar ────────────────────────────────────────────────────────
|
||||
hdr = QHBoxLayout()
|
||||
hdr.setContentsMargins(8, 6, 8, 6)
|
||||
hdr.setSpacing(6)
|
||||
|
||||
title = QLabel("👤 User & Privilege Management")
|
||||
f = title.font()
|
||||
f.setBold(True)
|
||||
f.setPointSize(11)
|
||||
title.setFont(f)
|
||||
title.setObjectName("structureTitle")
|
||||
|
||||
self._refresh_btn = QPushButton("🔄 Refresh")
|
||||
self._refresh_btn.setFixedWidth(90)
|
||||
self._refresh_btn.clicked.connect(self._load_users)
|
||||
|
||||
hdr.addWidget(title)
|
||||
hdr.addStretch()
|
||||
hdr.addWidget(self._refresh_btn)
|
||||
root.addLayout(hdr)
|
||||
|
||||
# ── Unsupported banner (hidden for MySQL/PG) ──────────────────────────
|
||||
self._unsupported_lbl = QLabel(
|
||||
"ℹ️ User management is not available for this database type.\n"
|
||||
" (Supports MySQL and PostgreSQL only.)"
|
||||
)
|
||||
self._unsupported_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self._unsupported_lbl.setObjectName("emptyLabel")
|
||||
root.addWidget(self._unsupported_lbl)
|
||||
|
||||
if self._db_type not in ("mysql", "postgresql"):
|
||||
self._unsupported_lbl.setVisible(True)
|
||||
return
|
||||
self._unsupported_lbl.setVisible(False)
|
||||
|
||||
# ── Main splitter: user list (left) / grants detail (right) ──────────
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
|
||||
# ── Left: user table ──────────────────────────────────────────────────
|
||||
left = QWidget()
|
||||
ll = QVBoxLayout(left)
|
||||
ll.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
ll.addWidget(self._make_section_label("Users"))
|
||||
|
||||
# Toolbar
|
||||
tb = QHBoxLayout()
|
||||
tb.setContentsMargins(4, 0, 4, 4)
|
||||
self._add_user_btn = QPushButton("➕ Create User")
|
||||
self._drop_user_btn = QPushButton("🗑️ Drop User")
|
||||
self._drop_user_btn.setObjectName("crudDeleteBtn")
|
||||
self._drop_user_btn.setEnabled(False)
|
||||
self._add_user_btn.clicked.connect(self._create_user)
|
||||
self._drop_user_btn.clicked.connect(self._drop_user)
|
||||
tb.addWidget(self._add_user_btn)
|
||||
tb.addWidget(self._drop_user_btn)
|
||||
tb.addStretch()
|
||||
ll.addLayout(tb)
|
||||
|
||||
self._user_table = QTableWidget()
|
||||
self._user_table.setAlternatingRowColors(True)
|
||||
self._user_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
self._user_table.setSelectionBehavior(
|
||||
QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self._user_table.setSelectionMode(
|
||||
QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self._user_table.verticalHeader().setDefaultSectionSize(24)
|
||||
self._user_table.itemSelectionChanged.connect(self._on_user_selected)
|
||||
ll.addWidget(self._user_table)
|
||||
|
||||
splitter.addWidget(left)
|
||||
|
||||
# ── Right: grants panel ───────────────────────────────────────────────
|
||||
right = QWidget()
|
||||
rl = QVBoxLayout(right)
|
||||
rl.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
rl.addWidget(self._make_section_label("Grants for selected user"))
|
||||
|
||||
# Grant toolbar
|
||||
gtb = QHBoxLayout()
|
||||
gtb.setContentsMargins(4, 0, 4, 4)
|
||||
self._grant_btn = QPushButton("+ Grant")
|
||||
self._revoke_btn = QPushButton("- Revoke")
|
||||
self._grant_btn.setEnabled(False)
|
||||
self._revoke_btn.setEnabled(False)
|
||||
self._grant_btn.clicked.connect(self._grant_privs)
|
||||
self._revoke_btn.clicked.connect(self._revoke_privs)
|
||||
gtb.addWidget(self._grant_btn)
|
||||
gtb.addWidget(self._revoke_btn)
|
||||
gtb.addStretch()
|
||||
rl.addLayout(gtb)
|
||||
|
||||
self._grants_table = QTableWidget(0, 1)
|
||||
self._grants_table.setHorizontalHeaderLabels(["GRANT Statement"])
|
||||
self._grants_table.horizontalHeader().setSectionResizeMode(
|
||||
QHeaderView.ResizeMode.Stretch)
|
||||
self._grants_table.setAlternatingRowColors(True)
|
||||
self._grants_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
self._grants_table.verticalHeader().setDefaultSectionSize(24)
|
||||
rl.addWidget(self._grants_table)
|
||||
|
||||
splitter.addWidget(right)
|
||||
splitter.setSizes([340, 560])
|
||||
|
||||
root.addWidget(splitter, 1)
|
||||
|
||||
# ── Status bar ────────────────────────────────────────────────────────
|
||||
self._status_lbl = QLabel("")
|
||||
self._status_lbl.setObjectName("rowCountLbl")
|
||||
self._status_lbl.setContentsMargins(8, 4, 0, 4)
|
||||
root.addWidget(self._status_lbl)
|
||||
|
||||
@staticmethod
|
||||
def _make_section_label(text: str) -> QLabel:
|
||||
lbl = QLabel(f" {text}")
|
||||
lbl.setObjectName("sidebarTitle")
|
||||
lbl.setContentsMargins(0, 4, 0, 4)
|
||||
f = lbl.font()
|
||||
f.setBold(True)
|
||||
lbl.setFont(f)
|
||||
return lbl
|
||||
|
||||
# ── Data loading ──────────────────────────────────────────────────────────
|
||||
|
||||
def _load_users(self):
|
||||
if self._db_type not in ("mysql", "postgresql"):
|
||||
return
|
||||
fn = _mysql_get_users if self._db_type == "mysql" else _pg_get_users
|
||||
self._worker = _UserWorker(fn, self._driver, parent=self)
|
||||
self._worker.result.connect(self._populate_users)
|
||||
self._worker.error.connect(
|
||||
lambda e: self._set_status(f"Error loading users: {e}"))
|
||||
self._worker.start()
|
||||
|
||||
def _load_databases(self):
|
||||
try:
|
||||
self._databases = self._driver.get_databases()
|
||||
except Exception:
|
||||
self._databases = []
|
||||
|
||||
def _populate_users(self, users: list[dict]):
|
||||
t = self._user_table
|
||||
t.clear()
|
||||
|
||||
if self._db_type == "mysql":
|
||||
t.setColumnCount(3)
|
||||
t.setHorizontalHeaderLabels(["Username", "Host", "Super"])
|
||||
else:
|
||||
t.setColumnCount(3)
|
||||
t.setHorizontalHeaderLabels(["Username", "Superuser", "CreateDB"])
|
||||
|
||||
t.setRowCount(len(users))
|
||||
t.horizontalHeader().setSectionResizeMode(
|
||||
QHeaderView.ResizeMode.ResizeToContents)
|
||||
t.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
|
||||
for r, u in enumerate(users):
|
||||
if self._db_type == "mysql":
|
||||
vals = [u["user"], u["host"], u.get("super", "")]
|
||||
else:
|
||||
vals = [u["user"], u.get("super", ""), u.get("createdb", "")]
|
||||
for c, v in enumerate(vals):
|
||||
item = QTableWidgetItem(str(v))
|
||||
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
t.setItem(r, c, item)
|
||||
|
||||
self._set_status(f"{len(users)} user(s)")
|
||||
|
||||
def _on_user_selected(self):
|
||||
rows = self._user_table.selectionModel().selectedRows()
|
||||
if not rows:
|
||||
self._drop_user_btn.setEnabled(False)
|
||||
self._grant_btn.setEnabled(False)
|
||||
self._revoke_btn.setEnabled(False)
|
||||
return
|
||||
|
||||
r = rows[0].row()
|
||||
self._current_user = self._user_table.item(r, 0).text()
|
||||
self._current_host = (
|
||||
self._user_table.item(r, 1).text()
|
||||
if self._db_type == "mysql"
|
||||
else ""
|
||||
)
|
||||
|
||||
self._drop_user_btn.setEnabled(True)
|
||||
self._grant_btn.setEnabled(True)
|
||||
self._revoke_btn.setEnabled(True)
|
||||
self._load_grants()
|
||||
|
||||
def _load_grants(self):
|
||||
if not self._current_user:
|
||||
return
|
||||
fn = _mysql_get_grants if self._db_type == "mysql" else _pg_get_grants
|
||||
self._worker = _UserWorker(
|
||||
fn, self._driver, self._current_user, self._current_host, parent=self
|
||||
)
|
||||
self._worker.result.connect(self._populate_grants)
|
||||
self._worker.error.connect(
|
||||
lambda e: self._set_status(f"Error loading grants: {e}"))
|
||||
self._worker.start()
|
||||
|
||||
def _populate_grants(self, grants: list[str]):
|
||||
t = self._grants_table
|
||||
t.setRowCount(len(grants))
|
||||
mono = QFont("Consolas", 10)
|
||||
for r, g in enumerate(grants):
|
||||
item = QTableWidgetItem(g)
|
||||
item.setFont(mono)
|
||||
t.setItem(r, 0, item)
|
||||
self._set_status(
|
||||
f"{len(grants)} grant statement(s) for {self._current_user}"
|
||||
)
|
||||
|
||||
# ── CRUD operations ───────────────────────────────────────────────────────
|
||||
|
||||
def _create_user(self):
|
||||
dlg = _CreateUserDialog(self._db_type, parent=self)
|
||||
if not dlg.exec():
|
||||
return
|
||||
fn = _mysql_create_user if self._db_type == "mysql" else _pg_create_user
|
||||
self._worker = _UserWorker(
|
||||
fn, self._driver, dlg.username, dlg.host, dlg.password, parent=self
|
||||
)
|
||||
self._worker.result.connect(lambda _: (
|
||||
self._set_status(f"User '{dlg.username}' created."),
|
||||
self._load_users(),
|
||||
))
|
||||
self._worker.error.connect(
|
||||
lambda e: QMessageBox.critical(self, "Create User Error", str(e))
|
||||
)
|
||||
self._worker.start()
|
||||
|
||||
def _drop_user(self):
|
||||
if not self._current_user:
|
||||
return
|
||||
btn = QMessageBox.warning(
|
||||
self, "Drop User",
|
||||
f"Drop user '{self._current_user}' permanently?\n\n"
|
||||
"All associated privileges will be revoked.",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if btn != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
fn = _mysql_drop_user if self._db_type == "mysql" else _pg_drop_user
|
||||
self._worker = _UserWorker(
|
||||
fn, self._driver, self._current_user, self._current_host, parent=self
|
||||
)
|
||||
self._worker.result.connect(lambda _: (
|
||||
self._set_status(f"User '{self._current_user}' dropped."),
|
||||
self._load_users(),
|
||||
))
|
||||
self._worker.error.connect(
|
||||
lambda e: QMessageBox.critical(self, "Drop User Error", str(e))
|
||||
)
|
||||
self._worker.start()
|
||||
|
||||
def _grant_privs(self):
|
||||
dlg = _GrantDialog(
|
||||
self._driver, self._db_type, self._databases,
|
||||
self._current_user, self._current_host, mode="grant", parent=self,
|
||||
)
|
||||
if dlg.exec():
|
||||
self._set_status(
|
||||
f"Privileges granted to {self._current_user}."
|
||||
)
|
||||
self._load_grants()
|
||||
|
||||
def _revoke_privs(self):
|
||||
dlg = _GrantDialog(
|
||||
self._driver, self._db_type, self._databases,
|
||||
self._current_user, self._current_host, mode="revoke", parent=self,
|
||||
)
|
||||
if dlg.exec():
|
||||
self._set_status(
|
||||
f"Privileges revoked from {self._current_user}."
|
||||
)
|
||||
self._load_grants()
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _set_status(self, msg: str):
|
||||
self._status_lbl.setText(msg)
|
||||
self.status_message.emit(msg)
|
||||
Reference in New Issue
Block a user