diff --git a/setoolsgui/apol.py b/setoolsgui/apol.py index c18b8e80..e6771030 100644 --- a/setoolsgui/apol.py +++ b/setoolsgui/apol.py @@ -18,7 +18,9 @@ # Supported analyses. These are not directly used here, but # will init the tab registry in widgets.tab for apol's analyses. -from .widgets import (fsusequery, +# pylint: disable=unused-import +from .widgets import (constraintquery, + fsusequery, genfsconquery, ibendportconquery, ibpkeyconquery, diff --git a/setoolsgui/widgets/constraintquery.py b/setoolsgui/widgets/constraintquery.py new file mode 100644 index 00000000..5bb816ef --- /dev/null +++ b/setoolsgui/widgets/constraintquery.py @@ -0,0 +1,152 @@ +# SPDX-License-Identifier: LGPL-2.1-only + +from PyQt6 import QtWidgets +import setools + +from . import criteria, models, tab + +__all__ = ("ConstraintQueryTab",) + + +class ConstraintQueryTab(tab.TableResultTabWidget): + + """A constraint query.""" + + section = tab.AnalysisSection.Rules + tab_title = "Constraints" + mlsonly = False + + def __init__(self, policy: "setools.SELinuxPolicy", _, /, *, + parent: QtWidgets.QWidget | None = None) -> None: + + super().__init__(setools.ConstraintQuery(policy), None, enable_criteria=True, + parent=parent) + + self.setWhatsThis("Search constraints in a SELinux policy.") + + # + # Set up criteria widgets + # + rt = criteria.ConstrainType("Rule Type", self.query, parent=self.criteria_frame) + rt.setToolTip("The rule types for constraint matching.") + rt.setWhatsThis( + """ +

Select rule types for constraint matching.

+ +

If a rule's has a one of the selected types, it will be returned.

+ """) + + user = criteria.UserNameWidget("User In Expression", + self.query, + "user", + enable_regex=True, + parent=self.criteria_frame) + user.setToolTip("Search for a user in the expression.") + user.setWhatsThis( + """ +

Search for users in a constraint expression..

+ +

If a constraint's expression has this user in its expression, + it will be returned.

+ """) + + role = criteria.RoleNameWidget("Role In Expression", + self.query, + "role", + enable_regex=True, + parent=self.criteria_frame) + role.setToolTip("Search for a role in the expression.") + role.setWhatsThis( + """ +

Search for roles in a constraint expression..

+ +

If a constraint's expression has this role in its expression, + it will be returned.

+ """) + + type_ = criteria.TypeOrAttrNameWidget("Type In Expression", + self.query, + "type_", + mode=criteria.TypeOrAttrNameWidget.Mode.type_only, + enable_regex=True, + enable_indirect=False, + parent=self.criteria_frame) + type_.setToolTip("Search for a type in the expression.") + type_.setWhatsThis( + """ +

Search for types in a constraint expression..

+ +

If a constraint's expression has this type in its expression, + it will be returned.

+ """) + + tclass = criteria.ObjClassCriteriaWidget("Object Class", + self.query, + "tclass", + parent=self.criteria_frame) + tclass.setToolTip("The object class(es) for constraint matching.") + tclass.setWhatsThis( + """ +

Select object classes for constraint matching.

+ +

A rule will be returned if its object class is one of the selected + classes

+ """) + + perms = criteria.PermissionCriteriaWidget("Permission Set", + self.query, + "perms", + enable_equal=True, + enable_subset=True, + parent=self.criteria_frame) + perms.setToolTip("The permission(s) for constraint matching.") + perms.setWhatsThis( + """ +

Select permissions for constraint matching.

+ +

Available permissions are dependent on the selected object + classes. If multiple classes are selected, only permissions + available in all of the classes are available.

+ """) + + # Connect signals + tclass.selectionChanged.connect(perms.set_classes) + + # Add widgets to layout + self.criteria_frame_layout.addWidget(rt, 0, 0, 1, 1) + self.criteria_frame_layout.addWidget(user, 0, 1, 1, 1) + self.criteria_frame_layout.addWidget(role, 1, 0, 1, 1) + self.criteria_frame_layout.addWidget(type_, 1, 1, 1, 1) + self.criteria_frame_layout.addWidget(tclass, 2, 0, 1, 1) + self.criteria_frame_layout.addWidget(perms, 2, 1, 1, 1) + self.criteria_frame_layout.addWidget(self.buttonBox, 3, 0, 1, 2) + + # Save widget references + self.criteria = (rt, user, role, type_, tclass, perms) + + # Set result table's model + self.table_results_model = models.ConstraintTable(self.table_results) + + +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 = ConstraintQueryTab(setools.SELinuxPolicy(), mw) + 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() + 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 75b1d081..f3dad5f9 100644 --- a/setoolsgui/widgets/criteria/__init__.py +++ b/setoolsgui/widgets/criteria/__init__.py @@ -4,6 +4,7 @@ from .boolean import * from .comboenum import * +from .constraintype import * from .context import * from .fsuseruletype import * from .infiniband import * diff --git a/setoolsgui/widgets/criteria/constraintype.py b/setoolsgui/widgets/criteria/constraintype.py new file mode 100644 index 00000000..182341c3 --- /dev/null +++ b/setoolsgui/widgets/criteria/constraintype.py @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: LGPL-2.1-only + +from PyQt6 import QtWidgets +import setools + +from .checkboxset import CheckboxSetCriteriaWidget + +DEFAULT_CHECKED = ("constrain",) + +__all__ = ('ConstrainType',) + + +class ConstrainType(CheckboxSetCriteriaWidget): + + """ + Criteria selection widget presenting type enforcement rule types as a series + of checkboxes. The selected checkboxes are then merged into a single Python + list consisting of object names (constraint types) and stored in the query's + specified attribute. + """ + + def __init__(self, title: str, query: setools.PolicyQuery, attrname: str = "ruletype", + parent: QtWidgets.QWidget | None = None) -> None: + + super().__init__(title, query, attrname, (rt.name for rt in setools.ConstraintRuletype), + num_cols=2, parent=parent) + + for name, widget in self.criteria.items(): + widget.setChecked(name in DEFAULT_CHECKED) + widget.setToolTip(f"Match {name} rules.") + widget.setWhatsThis( + f""" +

Match {name} rules

+ +

If a rule has the {name} rule type, it will be returned.

+ """) + + +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.ConstraintQuery(setools.SELinuxPolicy()) + + app = QtWidgets.QApplication(sys.argv) + mw = QtWidgets.QMainWindow() + w = ConstrainType("Test constrain ruletypes", q, parent=mw) + w.setToolTip("test tooltip") + w.setWhatsThis("test whats this") + mw.setCentralWidget(w) + mw.resize(w.size()) + whatsthis = QtWidgets.QWhatsThis.createAction(mw) + mw.menuBar().addAction(whatsthis) # type: ignore[union-attr] + mw.show() + rc = app.exec() + sys.exit(rc) diff --git a/setoolsgui/widgets/models/constraint.py b/setoolsgui/widgets/models/constraint.py index bf61bf89..ee05d00c 100644 --- a/setoolsgui/widgets/models/constraint.py +++ b/setoolsgui/widgets/models/constraint.py @@ -2,12 +2,20 @@ # # SPDX-License-Identifier: LGPL-2.1-only # -# + +import typing + from PyQt6 import QtCore import setools +from .. import details +from . import modelroles from .table import SEToolsTableModel +HAS_PERMS: typing.Final[tuple[setools.ConstraintRuletype, ...]] = ( + setools.ConstraintRuletype.constrain, + setools.ConstraintRuletype.mlsconstrain) + __all__ = ("ConstraintTable",) @@ -33,12 +41,52 @@ def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.ItemDataRole.Dis case 1: return rule.tclass.name case 2: - if rule.ruletype in (setools.ConstraintRuletype.constrain, - setools.ConstraintRuletype.mlsconstrain): + if rule.ruletype in HAS_PERMS: return ", ".join(sorted(rule.perms)) else: return None case 3: return str(rule.expression) + case modelroles.ContextMenuRole: + if col == 2: + return details.objclass_detail_action(rule.tclass) + + case QtCore.Qt.ItemDataRole.WhatsThisRole: + match col: + case 0: + column_whatsthis = \ + """ +

This is the type of constraint.

+ """ + case 1: + column_whatsthis = \ + """ +

This is the object class of the constraint.

+ """ + case 2: + if rule.ruletype in HAS_PERMS: + column_whatsthis = \ + """ +

These are the permissions of the constraint.

+ """ + else: + column_whatsthis = f"This column does not apply to {rule.ruletype}." + case 3: + column_whatsthis = \ + """ +

This expression of the constraint.

+ """ + case _: + column_whatsthis = "" + + return \ + f""" +

Table Representation of Constraints

+ +

Each part of the rule is represented as a column in the table.

+ + {column_whatsthis} + """ + return super().data(index, role) diff --git a/tests-gui/widgets/test_constraintquery.py b/tests-gui/widgets/test_constraintquery.py new file mode 100644 index 00000000..49f486e4 --- /dev/null +++ b/tests-gui/widgets/test_constraintquery.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: GPL-2.0-only +import typing + +from PyQt6 import QtWidgets +from pytestqt.qtbot import QtBot + +import setools +from setoolsgui.widgets.constraintquery import ConstraintQueryTab +from setoolsgui.widgets import models + +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 = ConstraintQueryTab(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 = ConstraintQueryTab(mock_policy, None) + qtbot.addWidget(widget) + + rt, user, role, type_, tclass, perms = widget.criteria + + assert widget.criteria_frame_layout.columnCount() == 2 + assert widget.criteria_frame_layout.rowCount() == 4 + assert widget.criteria_frame_layout.itemAtPosition(0, 0).widget() == rt + assert widget.criteria_frame_layout.itemAtPosition(0, 1).widget() == user + assert widget.criteria_frame_layout.itemAtPosition(1, 0).widget() == role + assert widget.criteria_frame_layout.itemAtPosition(1, 1).widget() == type_ + assert widget.criteria_frame_layout.itemAtPosition(2, 0).widget() == tclass + assert widget.criteria_frame_layout.itemAtPosition(2, 1).widget() == perms + assert widget.criteria_frame_layout.itemAtPosition(3, 0).widget() == widget.buttonBox + assert widget.criteria_frame_layout.itemAtPosition(3, 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 = ConstraintQueryTab(mock_policy, None) + qtbot.addWidget(widget) + + rt, user, role, type_, tclass, perms = widget.criteria + + assert isinstance(widget.query, setools.ConstraintQuery) + assert isinstance(widget.table_results_model, models.ConstraintTable) + assert rt.attrname == "ruletype" + assert user.attrname == "user" + assert role.attrname == "role" + assert type_.attrname == "type_" + assert tclass.attrname == "tclass" + assert perms.attrname == "perms"