diff --git a/data/detail_popup.ui b/data/detail_popup.ui
new file mode 100644
index 0000000..8c1230c
--- /dev/null
+++ b/data/detail_popup.ui
@@ -0,0 +1,51 @@
+
+
+ DetailsPopup
+
+
+
+ 0
+ 0
+ 400
+ 300
+
+
+
+ Dialog
+
+
+ -
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Close
+
+
+
+
+
+
+
+
+ buttonBox
+ clicked(QAbstractButton*)
+ DetailsPopup
+ close()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+
diff --git a/data/userquery.ui b/data/userquery.ui
new file mode 100644
index 0000000..897dec0
--- /dev/null
+++ b/data/userquery.ui
@@ -0,0 +1,633 @@
+
+
+ UserQueryTab
+
+
+
+ 0
+ 0
+ 774
+ 846
+
+
+
+ QAbstractScrollArea::AdjustToContents
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 772
+ 844
+
+
+
+
+ 0
+ 0
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 20
+
+
+
+
+ 11
+ 75
+ true
+
+
+
+ SELinux Users
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ User Browser
+
+
+
-
+
+
+
+
+
+
+ Qt::Vertical
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ Search Criteria
+
+
+ -
+
+
+ QDialogButtonBox::Apply
+
+
+
+ -
+
+
+
+ 16777215
+ 120
+
+
+
+ Default MLS Level
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 3
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 150
+ 20
+
+
+
+
+ 250
+ 16777215
+
+
+
+
+ -
+
+
+ The level criterion will match if it is equal to the user's default level.
+
+
+ Equal
+
+
+ true
+
+
+
+ -
+
+
+ The level criterion will match if it dominates the user's default level.
+
+
+ Dominate
+
+
+
+ -
+
+
+ The level criterion will match if it is dominated by the user's default level.
+
+
+ Dominated
+
+
+
+
+
+
+ -
+
+
+
+ 16777215
+ 120
+
+
+
+ MLS Range
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 3
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 150
+ 20
+
+
+
+
+ 250
+ 16777215
+
+
+
+
+ -
+
+
+ The range criterion will match if it is equal to the user's range.
+
+
+ Equal
+
+
+ true
+
+
+
+ -
+
+
+ The range criterion will match if it is a subset of the user's range.
+
+
+ Subset
+
+
+
+ -
+
+
+ The range criterion will match if it overlaps the user's range.
+
+
+ Overlap
+
+
+
+ -
+
+
+ The range criterion will match if it is a superset of the user's range.
+
+
+ Superset
+
+
+
+
+
+
+ -
+
+
+ Roles
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 3
+
+
-
+
+
+ Clear
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ Invert
+
+
+
+ -
+
+
+ A matching user will have a role set equal to the selected roles.
+
+
+ Equal
+
+
+
+ -
+
+
+ A matching user will have any of the selected roles.
+
+
+ Any
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 250
+ 16777215
+
+
+
+ Match the role set of the user.
+
+
+ QAbstractItemView::ExtendedSelection
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ 0
+
+
+
+ Results
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+ -
+
+
+ QAbstractScrollArea::AdjustIgnored
+
+
+ true
+
+
+ true
+
+
+
+
+
+
+
+ Raw Results
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ Monospace
+
+
+
+
+
+
+ QPlainTextEdit::NoWrap
+
+
+ true
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16
+ 16
+
+
+
+
+ 16
+ 16
+
+
+
+ Qt::TabFocus
+
+
+ QPushButton:flat { border: none; }
+
+
+
+
+
+
+ icons/expand_inactive.png
+ icons/expand_active.pngicons/expand_inactive.png
+
+
+
+ 16
+ 16
+
+
+
+ true
+
+
+ false
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 75
+ true
+
+
+
+ Notes
+
+
+
+ -
+
+
+
+ 0
+ 125
+
+
+
+ Optionally enter notes here about the query.
+
+
+
+
+ browser_search_splitter
+ notes
+ label
+ notes_expander
+ label_4
+
+
+
+
+ UserList
+ QListView
+ setoolsgui/apol/usermodel.h
+
+
+
+ users
+ roles
+ roles_any
+ roles_equal
+ clear_roles
+ invert_roles
+ level
+ level_exact
+ level_dom
+ level_domby
+ range_
+ range_exact
+ range_overlap
+ range_subset
+ range_superset
+ results_frame
+ table_results
+ raw_results
+ notes_expander
+ notes
+
+
+
+
+ notes_expander
+ toggled(bool)
+ notes
+ setVisible(bool)
+
+
+ 17
+ 833
+
+
+ 379
+ 910
+
+
+
+
+ clear_roles
+ clicked()
+ roles
+ clearSelection()
+
+
+ 429
+ 99
+
+
+ 319
+ 184
+
+
+
+
+
diff --git a/setoolsgui/apol/details.py b/setoolsgui/apol/details.py
new file mode 100644
index 0000000..7e90bc4
--- /dev/null
+++ b/setoolsgui/apol/details.py
@@ -0,0 +1,60 @@
+# Copyright 2016, Tresys Technology, LLC
+#
+# This file is part of SETools.
+#
+# SETools is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 2.1 of
+# the License, or (at your option) any later version.
+#
+# SETools is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with SETools. If not, see
+# .
+#
+import logging
+
+from PyQt5.QtGui import QFont
+from PyQt5.QtWidgets import QDialog
+
+from ..widget import SEToolsWidget
+
+
+class DetailsPopup(SEToolsWidget, QDialog):
+
+ """A generic non-modal popup with a text field to write detailed info."""
+ # TODO: make the font changes relative
+ # instead of setting absolute values
+
+ def __init__(self, parent, title=None):
+ super(DetailsPopup, self).__init__(parent)
+ self.log = logging.getLogger(self.__class__.__name__)
+ self.setupUi(title)
+
+ def setupUi(self, title):
+ self.load_ui("detail_popup.ui")
+
+ if title:
+ self.title = title
+
+ @property
+ def title(self):
+ self.windowTitle(self)
+
+ @title.setter
+ def title(self, text):
+ self.setWindowTitle(text)
+
+ def append(self, text):
+ self.contents.setFontWeight(QFont.Normal)
+ self.contents.setFontPointSize(9)
+ self.contents.append(text)
+
+ def append_header(self, text):
+ self.contents.setFontWeight(QFont.Black)
+ self.contents.setFontPointSize(11)
+ self.contents.append(text)
diff --git a/setoolsgui/apol/mainwindow.py b/setoolsgui/apol/mainwindow.py
index ce0b23f..3b7ad07 100644
--- a/setoolsgui/apol/mainwindow.py
+++ b/setoolsgui/apol/mainwindow.py
@@ -31,6 +31,7 @@ from .infoflow import InfoFlowAnalysisTab
from .mlsrulequery import MLSRuleQueryTab
from .rbacrulequery import RBACRuleQueryTab
from .terulequery import TERuleQueryTab
+from .userquery import UserQueryTab
class ApolMainWindow(SEToolsWidget, QMainWindow):
@@ -197,42 +198,15 @@ class ChooseAnalysis(SEToolsWidget, QDialog):
The item_mapping attribute will be populated to
map the tree list items to the analysis tab widgets.
"""
-# _components_map = {"Attributes (Type)": TERuleQueryTab,
-# "Booleans": TERuleQueryTab,
-# "Categories": TERuleQueryTab,
-# "Common Permission Sets": TERuleQueryTab,
-# "Object Classes": TERuleQueryTab,
-# "Policy Capabilities": TERuleQueryTab,
-# "Roles": TERuleQueryTab,
-# "Types": TERuleQueryTab,
-# "Users": TERuleQueryTab}
-#
-# _rule_map = {"TE Rules": TERuleQueryTab,
-# "RBAC Rules": TERuleQueryTab,
-# "MLS Rules": TERuleQueryTab,
-# "Constraints": TERuleQueryTab}
-#
-# _analysis_map = {"Domain Transition Analysis": TERuleQueryTab,
-# "Information Flow Analysis": TERuleQueryTab}
-#
-# _labeling_map = {"fs_use Statements": TERuleQueryTab,
-# "Genfscon Statements": TERuleQueryTab,
-# "Initial SID Statements": TERuleQueryTab,
-# "Netifcon Statements": TERuleQueryTab,
-# "Nodecon Statements": TERuleQueryTab,
-# "Portcon Statements": TERuleQueryTab}
-#
-# _analysis_choices = {"Components": _components_map,
-# "Rules": _rule_map,
-# "Analysis": _analysis_map,
-# "Labeling Statements": _labeling_map}
_analysis_map = {"Domain Transition Analysis": DomainTransitionAnalysisTab,
"Information Flow Analysis": InfoFlowAnalysisTab}
+ _components_map = {"Users": UserQueryTab}
_rule_map = {"MLS Rules": MLSRuleQueryTab,
"RBAC Rules": RBACRuleQueryTab,
"TE Rules": TERuleQueryTab}
- _analysis_choices = {"Rules": _rule_map,
+ _analysis_choices = {"Components": _components_map,
+ "Rules": _rule_map,
"Analyses": _analysis_map}
def __init__(self, parent):
diff --git a/setoolsgui/apol/usermodel.py b/setoolsgui/apol/usermodel.py
new file mode 100644
index 0000000..ff7e4cf
--- /dev/null
+++ b/setoolsgui/apol/usermodel.py
@@ -0,0 +1,132 @@
+# Copyright 2016, Tresys Technology, LLC
+#
+# This file is part of SETools.
+#
+# SETools is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 2.1 of
+# the License, or (at your option) any later version.
+#
+# SETools is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with SETools. If not, see
+# .
+#
+from PyQt5.QtCore import Qt, QAbstractTableModel, QModelIndex
+from PyQt5.QtGui import QCursor
+from PyQt5.QtWidgets import QAction, QListView, QMenu
+from setools.policyrep.exception import MLSDisabled
+
+from .details import DetailsPopup
+
+
+def user_detail(parent, user):
+ """
+ Create a dialog box for user details.
+
+ Parameters:
+ parent The parent Qt Widget
+ user The user
+ """
+
+ detail = DetailsPopup(parent, "SELinux user detail: {0}".format(user))
+ detail.append_header("Roles:")
+
+ for r in user.roles:
+ detail.append(" {0}".format(r))
+
+ try:
+ l = user.mls_level
+ r = user.mls_range
+ except MLSDisabled:
+ pass
+ else:
+ detail.append_header("\nDefault MLS Level:")
+ detail.append(" {0}".format(l))
+ detail.append_header("\nMLS Range:")
+ detail.append(" {0}".format(r))
+
+ detail.show()
+
+
+class UserList(QListView):
+
+ """A QListView widget for listing users."""
+
+ def __init__(self, parent):
+ super(UserList, self).__init__(parent)
+
+ # set up right-click context menu
+ self.get_detail = QAction("More details...", self)
+ self.menu = QMenu(self)
+ self.menu.addAction(self.get_detail)
+
+ def contextMenuEvent(self, event):
+ self.menu.popup(QCursor.pos())
+
+
+class UserTableModel(QAbstractTableModel):
+
+ """Table-based model for users."""
+
+ def __init__(self, parent, mls):
+ super(UserTableModel, self).__init__(parent)
+ self.resultlist = []
+ self.mls = mls
+
+ def headerData(self, section, orientation, role):
+ if role == Qt.DisplayRole and orientation == Qt.Horizontal:
+ if section == 0:
+ return "User Name"
+ elif section == 1:
+ return "Roles"
+ elif section == 2:
+ return "Default Level"
+ elif section == 3:
+ return "Range"
+ else:
+ raise ValueError("Invalid column number: {0}".format(section))
+
+ def columnCount(self, parent=QModelIndex()):
+ if self.mls:
+ return 4
+ else:
+ return 2
+
+ def rowCount(self, parent=QModelIndex()):
+ if self.resultlist:
+ return len(self.resultlist)
+ else:
+ return 0
+
+ def data(self, index, role):
+ if role == Qt.DisplayRole:
+ if not self.resultlist:
+ return None
+
+ row = index.row()
+ col = index.column()
+
+ if col == 0:
+ return str(self.resultlist[row])
+ elif col == 1:
+ return ", ".join(sorted(str(r) for r in self.resultlist[row].roles))
+ elif col == 2:
+ try:
+ return str(self.resultlist[row].mls_level)
+ except MLSDisabled:
+ return None
+ elif col == 3:
+ try:
+ return str(self.resultlist[row].mls_range)
+ except MLSDisabled:
+ return None
+ else:
+ raise ValueError("Invalid column number: {0}".format(col))
+ elif role == Qt.UserRole:
+ # get the whole rule for user role
+ return self.resultlist[row].statement()
diff --git a/setoolsgui/apol/userquery.py b/setoolsgui/apol/userquery.py
new file mode 100644
index 0000000..eb698cb
--- /dev/null
+++ b/setoolsgui/apol/userquery.py
@@ -0,0 +1,244 @@
+# Copyright 2016, Tresys Technology, LLC
+#
+# This file is part of SETools.
+#
+# SETools is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 2.1 of
+# the License, or (at your option) any later version.
+#
+# SETools is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with SETools. If not, see
+# .
+#
+
+import logging
+
+from PyQt5.QtCore import pyqtSignal, Qt, QObject, QSortFilterProxyModel, QStringListModel, QThread
+from PyQt5.QtGui import QPalette, QTextCursor
+from PyQt5.QtWidgets import QCompleter, QHeaderView, QMessageBox, QProgressDialog, QScrollArea
+from setools import UserQuery
+
+from ..widget import SEToolsWidget
+from .models import SEToolsListModel, invert_list_selection
+from .usermodel import UserTableModel, user_detail
+
+
+class UserQueryTab(SEToolsWidget, QScrollArea):
+
+ """User browser and query tab."""
+
+ def __init__(self, parent, policy, perm_map):
+ super(UserQueryTab, self).__init__(parent)
+ self.log = logging.getLogger(self.__class__.__name__)
+ self.policy = policy
+ self.query = UserQuery(policy)
+ self.setupUi()
+
+ def __del__(self):
+ self.thread.quit()
+ self.thread.wait(5000)
+
+ def setupUi(self):
+ self.load_ui("userquery.ui")
+
+ # populate user list
+ self.user_model = SEToolsListModel(self)
+ self.user_model.item_list = sorted(self.policy.users())
+ self.users.setModel(self.user_model)
+
+ # populate role list
+ self.role_model = SEToolsListModel(self)
+ self.role_model.item_list = sorted(r for r in self.policy.roles() if r != "object_r")
+ self.roles.setModel(self.role_model)
+
+ # set up results
+ self.table_results_model = UserTableModel(self, self.policy.mls)
+ self.sort_proxy = QSortFilterProxyModel(self)
+ self.sort_proxy.setSourceModel(self.table_results_model)
+ self.table_results.setModel(self.sort_proxy)
+
+ if self.policy.mls:
+ # setup indications of errors on level/range
+ self.orig_palette = self.level.palette()
+ self.error_palette = self.level.palette()
+ self.error_palette.setColor(QPalette.Base, Qt.red)
+ self.clear_level_error()
+ self.clear_range_error()
+ else:
+ # hide level and range criteria
+ self.level_criteria.setHidden(True)
+ self.range_criteria.setHidden(True)
+
+ # set up processing thread
+ self.thread = QThread()
+ self.worker = ResultsUpdater(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)
+
+ # Ensure settings are consistent with the initial .ui state
+ self.notes.setHidden(not self.notes_expander.isChecked())
+
+ # connect signals
+ self.users.doubleClicked.connect(self.get_detail)
+ self.users.get_detail.triggered.connect(self.get_detail)
+ self.roles.selectionModel().selectionChanged.connect(self.set_roles)
+ self.invert_roles.clicked.connect(self.invert_role_selection)
+ self.level.textEdited.connect(self.clear_level_error)
+ self.level.editingFinished.connect(self.set_level)
+ self.range_.textEdited.connect(self.clear_range_error)
+ self.range_.editingFinished.connect(self.set_range)
+ self.buttonBox.clicked.connect(self.run)
+
+ #
+ # User browser
+ #
+ def get_detail(self):
+ # .ui is set for single item selection.
+ index = self.users.selectedIndexes()[0]
+ item = self.user_model.data(index, Qt.UserRole)
+
+ self.log.debug("Generating detail window for {0}".format(item))
+ user_detail(self, item)
+
+ #
+ # Role criteria
+ #
+ def set_roles(self):
+ selected_roles = []
+ for index in self.roles.selectionModel().selectedIndexes():
+ selected_roles.append(self.role_model.data(index, Qt.UserRole))
+
+ self.query.roles = selected_roles
+
+ def invert_role_selection(self):
+ invert_list_selection(self.roles.selectionModel())
+
+ #
+ # Default level criteria
+ #
+ def clear_level_error(self):
+ self.level.setToolTip("Match the default level of the user.")
+ self.level.setPalette(self.orig_palette)
+
+ def set_level(self):
+ try:
+ self.query.level = self.level.text()
+ except Exception as ex:
+ self.log.info("Level criterion error: " + str(ex))
+ self.level.setToolTip("Error: " + str(ex))
+ self.level.setPalette(self.error_palette)
+
+ #
+ # Range criteria
+ #
+ def clear_range_error(self):
+ self.range_.setToolTip("Match the default range of the user.")
+ self.range_.setPalette(self.orig_palette)
+
+ def set_range(self):
+ try:
+ self.query.range_ = self.range_.text()
+ except Exception as ex:
+ self.log.info("Range criterion error: " + str(ex))
+ self.range_.setToolTip("Error: " + str(ex))
+ self.range_.setPalette(self.error_palette)
+
+ #
+ # Results runner
+ #
+
+ def run(self, button):
+ # right now there is only one button.
+ self.query.roles_equal = self.roles_equal.isChecked()
+ self.query.level_dom = self.level_dom.isChecked()
+ self.query.level_domby = self.level_domby.isChecked()
+ self.query.range_overlap = self.range_overlap.isChecked()
+ self.query.range_subset = self.range_subset.isChecked()
+ self.query.range_superset = self.range_superset.isChecked()
+
+ # start processing
+ self.busy.setLabelText("Processing query...")
+ self.busy.show()
+ self.raw_results.clear()
+ self.thread.start()
+
+ def update_complete(self):
+ # 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.Start)
+
+ self.busy.reset()
+
+
+class ResultsUpdater(QObject):
+
+ """
+ Thread for processing queries and updating result widgets.
+
+ Parameters:
+ query The query object
+ model The model for the results
+
+ Qt signals:
+ finished The update has completed.
+ raw_line (str) A string to be appended to the raw results.
+ """
+
+ finished = pyqtSignal()
+ raw_line = pyqtSignal(str)
+
+ def __init__(self, query, model):
+ super(ResultsUpdater, self).__init__()
+ self.query = query
+ self.table_results_model = model
+
+ def update(self):
+ """Run the query and update results."""
+ self.table_results_model.beginResetModel()
+
+ results = []
+ counter = 0
+
+ for counter, item in enumerate(self.query.results(), start=1):
+ results.append(item)
+
+ self.raw_line.emit(item.statement())
+
+ if QThread.currentThread().isInterruptionRequested():
+ break
+ elif not counter % 10:
+ # yield execution every 10 rules
+ QThread.yieldCurrentThread()
+
+ self.table_results_model.resultlist = results
+ self.table_results_model.endResetModel()
+
+ self.finished.emit()