diff --git a/setoolsgui/apol.py b/setoolsgui/apol.py index 7827c2d0..6d2b8676 100644 --- a/setoolsgui/apol.py +++ b/setoolsgui/apol.py @@ -20,6 +20,7 @@ # will init the tab registry in widgets.tab for apol's analyses. # pylint: disable=unused-import from .widgets import (boolquery, + commonquery, constraintquery, fsusequery, genfsconquery, diff --git a/setoolsgui/widgets/commonquery.py b/setoolsgui/widgets/commonquery.py new file mode 100644 index 00000000..ea80de9d --- /dev/null +++ b/setoolsgui/widgets/commonquery.py @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: LGPL-2.1-only + +from PyQt6 import QtWidgets +import setools + +from . import criteria, models, tab + +__all__ = ("CommonQueryTab",) + + +class CommonQueryTab(tab.TableResultTabWidget): + + """A common permission set query.""" + + section = tab.AnalysisSection.Components + tab_title = "Common Permision Sets" + mlsonly = False + + def __init__(self, policy: setools.SELinuxPolicy, _, /, *, + parent: QtWidgets.QWidget | None = None) -> None: + + super().__init__(setools.CommonQuery(policy), None, enable_criteria=True, + enable_browser=True, parent=parent) + + self.setWhatsThis("Search common permission sets in an SELinux policy.") + + # + # Set up criteria widgets + # + name = criteria.CommonName("Name", self.query, "name", enable_regex=True, + parent=self.criteria_frame) + name.setToolTip("Search for common permission sets by name.") + name.setWhatsThis("

Search for common permission set by name.

") + + perms = criteria.PermissionCriteriaWidget("Permissions", self.query, "perms", + enable_equal=True) + perms.setToolTip("Search for common permission sets by permissions.") + perms.setWhatsThis("

Search for common permission set by permissions.

") + + # Add widgets to layout + self.criteria_frame_layout.addWidget(name, 0, 0, 1, 1) + self.criteria_frame_layout.addWidget(perms, 0, 1, 1, 1) + self.criteria_frame_layout.addWidget(self.buttonBox, 1, 0, 1, 2) + + # Save widget references + self.criteria = (name, perms) + + # Set result table's model + self.table_results_model = models.CommonTable(self.table_results) + + # + # Set up browser + # + self.browser.setModel(models.CommonTable(self.browser, + data=sorted(self.query.policy.commons()))) + + +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 = CommonQueryTab(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/__init__.py b/setoolsgui/widgets/criteria/__init__.py index f3dad5f9..17da499c 100644 --- a/setoolsgui/widgets/criteria/__init__.py +++ b/setoolsgui/widgets/criteria/__init__.py @@ -4,6 +4,7 @@ from .boolean import * from .comboenum import * +from .common import * from .constraintype import * from .context import * from .fsuseruletype import * diff --git a/setoolsgui/widgets/criteria/common.py b/setoolsgui/widgets/criteria/common.py new file mode 100644 index 00000000..7e9a2f1e --- /dev/null +++ b/setoolsgui/widgets/criteria/common.py @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: LGPL-2.1-only + +from PyQt6 import QtCore, QtWidgets +import setools + +from .criteria import OptionsPlacement +from .name import NameCriteriaWidget + +# Regex for exact matches to roles +VALIDATE_EXACT = r"[A-Za-z0-9._-]*" + +__all__ = ("CommonName",) + + +class CommonName(NameCriteriaWidget): + + """ + Widget providing a QLineEdit that saves the input to the attributes + of the specified query. This supports inputs of common names. + """ + + indirect_toggled = QtCore.pyqtSignal(bool) + + def __init__(self, title: str, query: setools.PolicyQuery, attrname: str, /, *, + parent: QtWidgets.QWidget | None = None, + options_placement: OptionsPlacement = OptionsPlacement.RIGHT, + required: bool = False, enable_regex: bool = True): + + # Create completion list + completion = list[str](r.name for r in query.policy.commons()) + + super().__init__(title, query, attrname, completion, VALIDATE_EXACT, + enable_regex=enable_regex, required=required, parent=parent, + options_placement=options_placement) + + +if __name__ == '__main__': + import sys + import logging + import warnings + + logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s|%(levelname)s|%(name)s|%(message)s') + warnings.simplefilter("default") + + q = setools.CommonQuery(setools.SELinuxPolicy()) + + app = QtWidgets.QApplication(sys.argv) + mw = QtWidgets.QMainWindow() + widget = CommonName("Test Common", q, "name", parent=mw) + widget.setToolTip("test tooltip") + widget.setWhatsThis("test whats this") + mw.setCentralWidget(widget) + mw.resize(widget.size()) + whatsthis = QtWidgets.QWhatsThis.createAction(mw) + mw.menuBar().addAction(whatsthis) # type: ignore[union-attr] + mw.setStatusBar(QtWidgets.QStatusBar(mw)) + mw.show() + sys.exit(app.exec()) diff --git a/setoolsgui/widgets/details/__init__.py b/setoolsgui/widgets/details/__init__.py index 0ed9be27..454ffab6 100644 --- a/setoolsgui/widgets/details/__init__.py +++ b/setoolsgui/widgets/details/__init__.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: LGPL-2.1-only from .boolean import * +from .common import * from .context import * from .objclass import * from .role import * diff --git a/setoolsgui/widgets/details/common.py b/setoolsgui/widgets/details/common.py new file mode 100644 index 00000000..9afab36c --- /dev/null +++ b/setoolsgui/widgets/details/common.py @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: LGPL-2.1-only +from PyQt6 import QtGui, QtWidgets +import setools + +from . import util + +__all__ = ('common_detail', 'common_detail_action', 'common_tooltip') + + +def common_detail(common: setools.Common, parent: QtWidgets.QWidget | None = None) -> None: + """Display a dialog with common details.""" + + util.display_object_details( + f"{common} Details", + f""" +

Common Name

+

{common}

+ +

Permissions ({len(common.perms)})

+ + """, + parent) + + +def common_detail_action(common: setools.Common, + parent: QtWidgets.QWidget | None = None) -> QtGui.QAction: + """Return a QAction that, when triggered, opens a common detail popup.""" + a = QtGui.QAction(f"Properties of {common}") + a.triggered.connect(lambda x: common_detail(common, parent)) + return a + + +def common_tooltip(common: setools.Common) -> str: + """Return tooltip text for this common.""" + nperms = len(common.perms) + if nperms == 0: + return f"{common} is a common permission set with no permissions defined." + elif nperms > 5: + return f"{common} is a common permission set with {nperms} permissions defined." + else: + return f"{common} is a common permission set with permissions: " \ + f"{', '.join(common.perms)}" diff --git a/setoolsgui/widgets/models/common.py b/setoolsgui/widgets/models/common.py index fda1e7c6..3aa0fee0 100644 --- a/setoolsgui/widgets/models/common.py +++ b/setoolsgui/widgets/models/common.py @@ -6,7 +6,9 @@ from PyQt6 import QtCore import setools +from . import modelroles from .table import SEToolsTableModel +from .. import details __all__ = ("CommonTable",) @@ -33,4 +35,10 @@ def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.ItemDataRole.Dis case 1: return ", ".join(sorted(item.perms)) + case modelroles.ContextMenuRole: + return (details.common_detail_action(item), ) + + case QtCore.Qt.ItemDataRole.ToolTipRole: + return details.common_tooltip(item) + return super().data(index, role) diff --git a/tests-gui/conftest.py b/tests-gui/conftest.py index 202191be..beb974a4 100644 --- a/tests-gui/conftest.py +++ b/tests-gui/conftest.py @@ -62,6 +62,7 @@ def mock_policy() -> Mock: policy = Mock(setools.SELinuxPolicy) policy.bools.return_value = (foo_bool, bar_bool) policy.classes.return_value = (foo_class, bar_class) + policy.commons.return_value = (common,) policy.roles.return_value = (foo_r, bar_r) policy.types.return_value = (foo_t, bar_t) policy.typeattributes.return_value = (fooattr, barattr) diff --git a/tests-gui/widgets/criteria/test_common.py b/tests-gui/widgets/criteria/test_common.py new file mode 100644 index 00000000..1f9dded2 --- /dev/null +++ b/tests-gui/widgets/criteria/test_common.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: GPL-2.0-only +from PyQt6 import QtCore +import pytest +from pytestqt.qtbot import QtBot + +from setoolsgui.widgets import criteria + + +@pytest.fixture +def widget(mock_query, request: pytest.FixtureRequest, qtbot: QtBot) -> criteria.CommonName: + """Pytest fixture to set up the widget.""" + marker = request.node.get_closest_marker("obj_args") + kwargs = marker.kwargs if marker else {} + w = criteria.CommonName(request.node.name, mock_query, "name", **kwargs) + qtbot.addWidget(w) + w.show() + return w + + +def test_base_settings(widget: criteria.CommonName, mock_query) -> None: + """Test base properties of widget.""" + model = widget.criteria.completer().model() + assert isinstance(model, QtCore.QStringListModel) + assert sorted(c.name for c in mock_query.policy.commons()) == model.stringList() diff --git a/tests-gui/widgets/test_commonquery.py b/tests-gui/widgets/test_commonquery.py new file mode 100644 index 00000000..5995c747 --- /dev/null +++ b/tests-gui/widgets/test_commonquery.py @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: GPL-2.0-only +import typing + +from PyQt6 import QtWidgets +import pytest +from pytestqt.qtbot import QtBot + +import setools +from setoolsgui.widgets.commonquery import CommonQueryTab + + +@pytest.fixture +def widget(mock_policy, request: pytest.FixtureRequest, qtbot: QtBot) -> CommonQueryTab: + """Pytest fixture to set up the widget.""" + marker = request.node.get_closest_marker("obj_args") + kwargs = marker.kwargs if marker else {} + w = CommonQueryTab(mock_policy, None, **kwargs) + qtbot.addWidget(w) + w.show() + return w + + +def test_docs(widget: CommonQueryTab) -> None: + """Check that docs are provided for the 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(widget: CommonQueryTab) -> None: + """Test the layout of the criteria frame.""" + name, perms = 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() == perms + 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(widget: CommonQueryTab) -> None: + """Test that widgets save to the correct query fields.""" + name, state = widget.criteria + + assert isinstance(widget.query, setools.CommonQuery) + assert name.attrname == "name" + assert state.attrname == "perms"