diff --git a/setoolsgui/apol.py b/setoolsgui/apol.py index e677103..7827c2d 100644 --- a/setoolsgui/apol.py +++ b/setoolsgui/apol.py @@ -19,7 +19,8 @@ from . import config, widgets # Supported analyses. These are not directly used here, but # will init the tab registry in widgets.tab for apol's analyses. # pylint: disable=unused-import -from .widgets import (constraintquery, +from .widgets import (boolquery, + constraintquery, fsusequery, genfsconquery, ibendportconquery, diff --git a/setoolsgui/apol/boolquery.py b/setoolsgui/apol/boolquery.py deleted file mode 100644 index 6116fe4..0000000 --- a/setoolsgui/apol/boolquery.py +++ /dev/null @@ -1,192 +0,0 @@ -# Copyright 2016, Tresys Technology, LLC -# -# SPDX-License-Identifier: LGPL-2.1-only -# -# - -import logging -from contextlib import suppress - -from PyQt5.QtCore import Qt, QSortFilterProxyModel, QStringListModel, QThread -from PyQt5.QtGui import QPalette, QTextCursor -from PyQt5.QtWidgets import QCompleter, QHeaderView, QMessageBox, QProgressDialog -from setools import BoolQuery - -from ..logtosignal import LogHandlerToSignal -from ..models import SEToolsListModel, invert_list_selection -from ..boolmodel import BooleanTableModel, boolean_detail -from .analysistab import AnalysisSection, AnalysisTab -from .exception import TabFieldError -from .queryupdater import QueryResultsUpdater -from .workspace import load_checkboxes, load_lineedits, load_textedits, \ - save_checkboxes, save_lineedits, save_textedits - - -class BoolQueryTab(AnalysisTab): - - """Bool browser and query tab.""" - - section = AnalysisSection.Components - tab_title = "Booleans" - mlsonly = False - - def __init__(self, parent, policy, perm_map): - super(BoolQueryTab, self).__init__(parent) - self.log = logging.getLogger(__name__) - self.policy = policy - self.query = BoolQuery(policy) - self.setupUi() - - def __del__(self): - with suppress(RuntimeError): - self.thread.quit() - self.thread.wait(5000) - - logging.getLogger("setools.boolquery").removeHandler(self.handler) - - def setupUi(self): - self.load_ui("apol/boolquery.ui") - - # populate bool list - self.bool_model = SEToolsListModel(self) - self.bool_model.item_list = sorted(r for r in self.policy.bools()) - self.bools.setModel(self.bool_model) - - # set up results - self.table_results_model = BooleanTableModel(self) - self.sort_proxy = QSortFilterProxyModel(self) - self.sort_proxy.setSourceModel(self.table_results_model) - self.table_results.setModel(self.sort_proxy) - self.table_results.sortByColumn(0, Qt.SortOrder.AscendingOrder) - - # setup indications of errors on level/range - self.errors = set() - self.orig_palette = self.name.palette() - self.error_palette = self.name.palette() - self.error_palette.setColor(QPalette.ColorRole.Base, Qt.GlobalColor.red) - self.clear_name_error() - - # set up processing thread - self.thread = QThread() - self.worker = QueryResultsUpdater(self.query, self.table_results_model) - self.worker.moveToThread(self.thread) - self.worker.raw_line.connect(self.raw_results.appendPlainText) - self.worker.finished.connect(self.update_complete) - self.worker.finished.connect(self.thread.quit) - self.thread.started.connect(self.worker.update) - - # create a "busy, please wait" dialog - self.busy = QProgressDialog(self) - self.busy.setModal(True) - self.busy.setRange(0, 0) - self.busy.setMinimumDuration(0) - self.busy.canceled.connect(self.thread.requestInterruption) - self.busy.reset() - - # update busy dialog from query INFO logs - self.handler = LogHandlerToSignal() - self.handler.message.connect(self.busy.setLabelText) - logging.getLogger("setools.boolquery").addHandler(self.handler) - - # Ensure settings are consistent with the initial .ui state - self.notes.setHidden(not self.notes_expander.isChecked()) - - # connect signals - self.bools.doubleClicked.connect(self.get_detail) - self.bools.get_detail.triggered.connect(self.get_detail) - self.name.textEdited.connect(self.clear_name_error) - self.name.editingFinished.connect(self.set_name) - self.name_regex.toggled.connect(self.set_name_regex) - self.buttonBox.clicked.connect(self.run) - - # - # Booleans browser - # - def get_detail(self): - # .ui is set for single item selection. - index = self.bools.selectedIndexes()[0] - item = self.bool_model.data(index, Qt.ItemDataRole.UserRole) - - self.log.debug("Generating detail window for {0}".format(item)) - boolean_detail(self, item) - - # - # Name criteria - # - def clear_name_error(self): - self.clear_criteria_error(self.name, "Match the Boolean name.") - - def set_name(self): - try: - self.query.name = self.name.text() - except Exception as ex: - self.log.error("Boolean name error: {0}".format(ex)) - self.set_criteria_error(self.name, ex) - - def set_name_regex(self, state): - self.log.debug("Setting name_regex {0}".format(state)) - self.query.name_regex = state - self.clear_name_error() - self.set_name() - - # - # Save/Load tab - # - def save(self): - """Return a dictionary of settings.""" - if self.errors: - raise TabFieldError("Field(s) are in error: {0}". - format(" ".join(o.objectName() for o in self.errors))) - - settings = {} - save_checkboxes(self, settings, ["criteria_expander", "notes_expander", - "default_any", "default_true", "default_false", - "name_regex"]) - save_lineedits(self, settings, ["name"]) - save_textedits(self, settings, ["notes"]) - return settings - - def load(self, settings): - load_checkboxes(self, settings, ["criteria_expander", "notes_expander", - "default_any", "default_true", "default_false", - "name_regex"]) - load_lineedits(self, settings, ["name"]) - load_textedits(self, settings, ["notes"]) - - # - # Results runner - # - - def run(self, button): - # right now there is only one button. - if self.default_any.isChecked(): - self.query.default = None - else: - self.query.default = self.default_true.isChecked() - - # start processing - self.busy.setLabelText("Processing query...") - self.busy.show() - self.raw_results.clear() - self.thread.start() - - def update_complete(self, count): - self.log.info("{0} Boolean(s) found.".format(count)) - - # update sizes/location of result displays - if not self.busy.wasCanceled(): - self.busy.setLabelText("Resizing the result table's columns; GUI may be unresponsive") - self.busy.repaint() - self.table_results.resizeColumnsToContents() - - if not self.busy.wasCanceled(): - self.busy.setLabelText("Resizing the result table's rows; GUI may be unresponsive") - self.busy.repaint() - self.table_results.resizeRowsToContents() - - if not self.busy.wasCanceled(): - self.busy.setLabelText("Moving the raw result to top; GUI may be unresponsive") - self.busy.repaint() - self.raw_results.moveCursor(QTextCursor.MoveOperation.Start) - - self.busy.reset() diff --git a/setoolsgui/apol/boolquery.ui b/setoolsgui/apol/boolquery.ui deleted file mode 100644 index 0acb7b3..0000000 --- a/setoolsgui/apol/boolquery.ui +++ /dev/null @@ -1,459 +0,0 @@ - - - BoolQueryTab_ui - - - - 0 - 0 - 774 - 846 - - - - QAbstractScrollArea::AdjustToContents - - - true - - - - - 0 - 0 - 772 - 844 - - - - - 0 - 0 - - - - - - - - 0 - 0 - - - - - 16777215 - 20 - - - - - 11 - 75 - true - - - - Booleans - - - - - - - - 0 - 80 - - - - Optionally enter notes here about the query. - - - Enter notes here. - - - - - - - Qt::Horizontal - - - - 440 - 20 - - - - - - - - Show: - - - - - - - Show or hide the search criteria (no settings are lost) - - - Criteria - - - true - - - - - - - Show or hide the notes field (no data is lost) - - - Notes - - - - - - - - 0 - 1 - - - - Qt::Horizontal - - - - Boolean Browser - - - - - - - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Search Criteria - - - - - - - 16777215 - 120 - - - - Boolean Name - - - - 6 - - - 6 - - - 6 - - - 6 - - - 3 - - - - - - 0 - 0 - - - - - 150 - 20 - - - - - 250 - 16777215 - - - - - - - - Use regular expressions to match the user's name. - - - Regex - - - - - - - - - - - 120 - 0 - - - - Default State - - - - - - Any - - - true - - - - - - - True - - - - - - - False - - - - - - - - - - QDialogButtonBox::Apply - - - - - - - - - - - 0 - 1 - - - - 0 - - - - - 0 - 0 - - - - Results - - - - 6 - - - 6 - - - 6 - - - 6 - - - - - - 0 - 0 - - - - QAbstractScrollArea::AdjustIgnored - - - true - - - true - - - - - - - - - 0 - 0 - - - - Raw Results - - - - 6 - - - 6 - - - 6 - - - 6 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - Monospace - - - - - - - QPlainTextEdit::NoWrap - - - true - - - - - - - - - - - - - splitter - notes - label - label_2 - horizontalSpacer - criteria_expander - notes_expander - - - - - SEToolsTableView - QTableView -
setoolsgui/tableview.h
-
- - GetDetailsListView - QListView -
setoolsgui/getdetailslist.h
-
-
- - criteria_expander - notes_expander - bools - name - name_regex - default_any - default_true - default_false - results_frame - table_results - raw_results - notes - - - - - notes_expander - toggled(bool) - notes - setVisible(bool) - - - 732 - 20 - - - 386 - 754 - - - - - criteria_expander - toggled(bool) - criteria_frame - setVisible(bool) - - - 583 - 20 - - - 496 - 226 - - - - -
diff --git a/setoolsgui/widgets/boolquery.py b/setoolsgui/widgets/boolquery.py new file mode 100644 index 0000000..8f33339 --- /dev/null +++ b/setoolsgui/widgets/boolquery.py @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: LGPL-2.1-only + +from PyQt6 import QtWidgets +import setools + +from . import criteria, models, tab + +__all__ = ("BoolQueryTab",) + + +class BoolQueryTab(tab.TableResultTabWidget): + + """A boolean query.""" + + section = tab.AnalysisSection.Components + tab_title = "Booleans" + mlsonly = False + + def __init__(self, policy: setools.SELinuxPolicy, _, /, *, + parent: QtWidgets.QWidget | None = None) -> None: + + super().__init__(setools.BoolQuery(policy), None, enable_criteria=True, + enable_browser=True, parent=parent) + + self.setWhatsThis("Search Booleans in a SELinux policy.") + + # + # Set up criteria widgets + # + name = criteria.BooleanNameCriteriaWidget("Name", self.query, "name", + enable_regex=True, + parent=self.criteria_frame) + name.setToolTip("Search for Booleans by name.") + name.setWhatsThis("

Search for Booleans by name.

") + + state = criteria.BooleanState("Default State", self.query, "default", + enable_any=True, + parent=self.criteria_frame) + state.setToolTip("Search for Booleans by default state.") + state.setWhatsThis("

Search for Booleans by default state.

") + + # Add widgets to layout + self.criteria_frame_layout.addWidget(name, 0, 0, 1, 1) + self.criteria_frame_layout.addWidget(state, 0, 1, 1, 1) + self.criteria_frame_layout.addWidget(self.buttonBox, 1, 0, 1, 2) + + # Save widget references + self.criteria = (name, state) + + # Set result table's model + self.table_results_model = models.BooleanTable(self.table_results) + + # + # Set up browser + # + self.browser.setModel(models.BooleanTable(self.browser, + data=sorted(self.query.policy.bools()))) + + +if __name__ == '__main__': + import sys + import warnings + import pprint + import logging + + logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s|%(levelname)s|%(name)s|%(message)s') + warnings.simplefilter("default") + + app = QtWidgets.QApplication(sys.argv) + mw = QtWidgets.QMainWindow() + widget = BoolQueryTab(setools.SELinuxPolicy(), mw) + mw.setCentralWidget(widget) + mw.resize(1280, 1024) + whatsthis = QtWidgets.QWhatsThis.createAction(mw) + mw.menuBar().addAction(whatsthis) # type: ignore[union-attr] + mw.setStatusBar(QtWidgets.QStatusBar(mw)) + mw.show() + rc = app.exec() + pprint.pprint(widget.save()) + sys.exit(rc) diff --git a/setoolsgui/widgets/criteria/boolean.py b/setoolsgui/widgets/criteria/boolean.py index 2e1ee48..95176df 100644 --- a/setoolsgui/widgets/criteria/boolean.py +++ b/setoolsgui/widgets/criteria/boolean.py @@ -1,15 +1,17 @@ # SPDX-License-Identifier: LGPL-2.1-only from PyQt6 import QtWidgets +import setools from .. import models +from .combobox import ComboBoxWidget from .list import ListCriteriaWidget from .name import NameCriteriaWidget # Regex for exact matches to types/attrs VALIDATE_EXACT = r"[A-Za-z0-9._-]*" -__all__ = ("BooleanListCriteriaWidget", "BooleanNameCriteriaWidget",) +__all__ = ("BooleanListCriteriaWidget", "BooleanNameCriteriaWidget", "BooleanState") class BooleanListCriteriaWidget(ListCriteriaWidget): @@ -48,12 +50,24 @@ class BooleanNameCriteriaWidget(NameCriteriaWidget): enable_regex=enable_regex, required=required, parent=parent) +class BooleanState(ComboBoxWidget): + + """Criteria selection widget presenting possible Boolean states.""" + + def __init__(self, title: str, query: setools.PolicyQuery, attrname: str, /, *, + enable_any: bool = True, parent: QtWidgets.QWidget | None = None) -> None: + + super().__init__(title, query, attrname, enable_any=enable_any, parent=parent) + + self.criteria.addItem("False", False) + self.criteria.addItem("True", True) + + if __name__ == '__main__': import sys import logging import warnings import setools - import pprint logging.basicConfig(level=logging.DEBUG, format='%(asctime)s|%(levelname)s|%(name)s|%(message)s') @@ -69,8 +83,10 @@ if __name__ == '__main__': layout = QtWidgets.QHBoxLayout(window) widget1 = BooleanListCriteriaWidget("Test Booleans list", q1, "boolean", parent=window) widget2 = BooleanNameCriteriaWidget("Test Booleans linedit", q2, "name", parent=window) + widget3 = BooleanState("Test Booleans State", q2, "default", enable_any=True, parent=window) layout.addWidget(widget1) layout.addWidget(widget2) + layout.addWidget(widget3) window.setToolTip("test tooltip") window.setWhatsThis("test whats this") mw.setCentralWidget(window) @@ -79,6 +95,4 @@ if __name__ == '__main__': mw.menuBar().addAction(whatsthis) # type: ignore[union-attr] mw.show() rc = app.exec() - print("Query settings:") - pprint.pprint(q1.boolean) sys.exit(rc) diff --git a/tests-gui/widgets/test_boolquery.py b/tests-gui/widgets/test_boolquery.py new file mode 100644 index 0000000..f4a6591 --- /dev/null +++ b/tests-gui/widgets/test_boolquery.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: GPL-2.0-only +import typing + +from PyQt6 import QtWidgets +from pytestqt.qtbot import QtBot + +import setools +from setoolsgui.widgets.boolquery import BoolQueryTab + +from .criteria.util import build_mock_policy + + +def test_docs(qtbot: QtBot) -> None: + """Check that docs are provided for the widget.""" + mock_policy = build_mock_policy() + widget = BoolQueryTab(mock_policy, None) + qtbot.addWidget(widget) + + assert widget.whatsThis() + assert widget.table_results.whatsThis() + assert widget.raw_results.whatsThis() + + for w in widget.criteria: + assert w.toolTip() + assert w.whatsThis() + + results = typing.cast(QtWidgets.QTabWidget, widget.results) + for index in range(results.count()): + assert results.tabWhatsThis(index) + + +def test_layout(qtbot: QtBot) -> None: + """Test the layout of the criteria frame.""" + mock_policy = build_mock_policy() + widget = BoolQueryTab(mock_policy, None) + qtbot.addWidget(widget) + + name, state = widget.criteria + + assert widget.criteria_frame_layout.columnCount() == 2 + assert widget.criteria_frame_layout.rowCount() == 2 + assert widget.criteria_frame_layout.itemAtPosition(0, 0).widget() == name + assert widget.criteria_frame_layout.itemAtPosition(0, 1).widget() == state + assert widget.criteria_frame_layout.itemAtPosition(1, 0).widget() == widget.buttonBox + assert widget.criteria_frame_layout.itemAtPosition(1, 1).widget() == widget.buttonBox + + +def test_criteria_mapping(qtbot: QtBot) -> None: + """Test that widgets save to the correct query fields.""" + mock_policy = build_mock_policy() + widget = BoolQueryTab(mock_policy, None) + qtbot.addWidget(widget) + + name, state = widget.criteria + + assert isinstance(widget.query, setools.BoolQuery) + assert name.attrname == "name" + assert state.attrname == "default"