632 lines
23 KiB
Python
632 lines
23 KiB
Python
"""
|
||
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)
|