feat: mask secret values in the UI and redact them from diagnostics
- Env/header values whose key looks secret (TOKEN, API_KEY, PASSWORD, AUTH, ...) render as •••••••• via a display-only delegate; a 'Show secrets' toggle reveals them. Underlying data, editing, and save are untouched. - The Add dialog switches the value field to password echo when the key name looks secret. - Copy-diagnostics now redacts secrets from args (--token <v>, --api-key=<v>, and well-known token prefixes like ghp_/sk-/xoxb-), since those reports get pasted into public bug reports. Env and header values were already omitted from diagnostics.
This commit is contained in:
@@ -35,6 +35,7 @@ from PySide6.QtWidgets import (
|
||||
QPushButton,
|
||||
QSplitter,
|
||||
QStackedWidget,
|
||||
QStyledItemDelegate,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QVBoxLayout,
|
||||
@@ -134,6 +135,25 @@ class ConnTester(QThread):
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Small reusable: key/value editor (for env and headers)
|
||||
# --------------------------------------------------------------------------- #
|
||||
class _SecretMaskDelegate(QStyledItemDelegate):
|
||||
"""Masks the DISPLAY of values whose key looks like a secret (API_KEY,
|
||||
TOKEN, ...). The underlying item text stays real, so editing, dump() and
|
||||
save are untouched — only what's painted on screen changes."""
|
||||
|
||||
def __init__(self, table):
|
||||
super().__init__(table)
|
||||
self._table = table
|
||||
self.revealed = False
|
||||
|
||||
def initStyleOption(self, option, index):
|
||||
super().initStyleOption(option, index)
|
||||
if self.revealed or not option.text:
|
||||
return
|
||||
key_item = self._table.item(index.row(), 0)
|
||||
if key_item and core.is_secret_key(key_item.text()):
|
||||
option.text = core.MASK
|
||||
|
||||
|
||||
class KeyValueTable(QWidget):
|
||||
def __init__(self, key_label="Key", val_label="Value", on_change=None, before_change=None):
|
||||
super().__init__()
|
||||
@@ -154,17 +174,29 @@ class KeyValueTable(QWidget):
|
||||
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.table.setMinimumHeight(90)
|
||||
self.table.itemChanged.connect(self._changed)
|
||||
# Secret-looking values (API_KEY, TOKEN, ...) render masked by default.
|
||||
self._mask_delegate = _SecretMaskDelegate(self.table)
|
||||
self.table.setItemDelegateForColumn(1, self._mask_delegate)
|
||||
lay.addWidget(self.table, 1)
|
||||
row = QHBoxLayout()
|
||||
add = QPushButton("+ Add")
|
||||
rem = QPushButton("− Remove")
|
||||
add.clicked.connect(self._add_row)
|
||||
rem.clicked.connect(self._remove_row)
|
||||
self.reveal_btn = QPushButton("Show secrets")
|
||||
self.reveal_btn.setCheckable(True)
|
||||
self.reveal_btn.toggled.connect(self._toggle_reveal)
|
||||
row.addWidget(add)
|
||||
row.addWidget(rem)
|
||||
row.addStretch()
|
||||
row.addWidget(self.reveal_btn)
|
||||
lay.addLayout(row)
|
||||
|
||||
def _toggle_reveal(self, on):
|
||||
self._mask_delegate.revealed = on
|
||||
self.reveal_btn.setText("Hide secrets" if on else "Show secrets")
|
||||
self.table.viewport().update()
|
||||
|
||||
def _changed(self, *_):
|
||||
if self._on_change:
|
||||
self._on_change()
|
||||
@@ -195,6 +227,15 @@ class KeyValueTable(QWidget):
|
||||
grid.addWidget(val_edit, 1, 1)
|
||||
v.addLayout(grid)
|
||||
|
||||
# Type an API_KEY/TOKEN-style name and the value field masks itself.
|
||||
def _sync_echo(text):
|
||||
secret = core.is_secret_key(text)
|
||||
val_edit.setEchoMode(
|
||||
QLineEdit.EchoMode.Password if secret else QLineEdit.EchoMode.Normal
|
||||
)
|
||||
|
||||
key_edit.textChanged.connect(_sync_echo)
|
||||
|
||||
btns = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user