Import new rpc confirmation window code
Import unmodified implementation done by @boring-stuff. Full history for reference is available in rpc-confirmation-window branch. QubesOS/qubes-issues#910
This commit is contained in:
parent
83526a28d3
commit
b3ceb2d7fa
359
qubespolicy/glade/RPCConfirmationWindow.glade
Normal file
359
qubespolicy/glade/RPCConfirmationWindow.glade
Normal file
@ -0,0 +1,359 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.18.3 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.12"/>
|
||||
<object class="GtkWindow" id="RPCConfirmationWindow">
|
||||
<property name="width_request">400</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Operation execution</property>
|
||||
<property name="window_position">center</property>
|
||||
<property name="icon_name">dialog-question</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="urgency_hint">True</property>
|
||||
<property name="gravity">center</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="WindowBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="ErrorBar">
|
||||
<property name="app_paintable">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="message_type">error</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="ActionArea">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox" id="ContentArea">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="ErrorImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-dialog-error</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ErrorMessage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">ErrorMessage</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="MainBox">
|
||||
<property name="width_request">100</property>
|
||||
<property name="height_request">80</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">12</property>
|
||||
<property name="margin_right">12</property>
|
||||
<property name="margin_top">12</property>
|
||||
<property name="margin_bottom">12</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="ButtonBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancelButton">
|
||||
<property name="label">gtk-cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="okButton">
|
||||
<property name="label">gtk-ok</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="ContentBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="AlwaysShownBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_bottom">6</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="RPCConfirmationIcon">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-dialog-question</property>
|
||||
<property name="icon_size">6</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="rpcDescription">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Do you want to allow the following operation?
|
||||
<small>Select the target domain and confirm with 'OK'</small></property>
|
||||
<property name="use_markup">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGrid" id="grid1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">12</property>
|
||||
<property name="margin_bottom">6</property>
|
||||
<property name="row_spacing">12</property>
|
||||
<property name="column_spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="TargetDescLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">1</property>
|
||||
<property name="label" translatable="yes">Target:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="TargetCombo">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="has_entry">True</property>
|
||||
<child internal-child="entry">
|
||||
<object class="GtkEntry" id="TargetComboEntry">
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="placeholder_text" translatable="yes">Start typing or use the arrow</property>
|
||||
<property name="input_hints">GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_NONE</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sourceDescLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">1</property>
|
||||
<property name="label" translatable="yes">Source:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="sourceEntry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="text" translatable="yes">source</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="operationDescLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">1</property>
|
||||
<property name="label" translatable="yes">Operation:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="rpcLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">qubes.<b>MyOperation</b></property>
|
||||
<property name="use_markup">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkExpander" id="AdvancedSection">
|
||||
<property name="can_focus">True</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="AdvancedOptions">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">6</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="DisplayTemplates">
|
||||
<property name="label" translatable="yes">Display templates in the target list</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="CustomLocation">
|
||||
<property name="label" translatable="yes">Choose a custom destination in the target</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="AdvancedLabel">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Advanced options</property>
|
||||
<property name="use_underline">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
301
qubespolicy/gtkhelpers.py
Normal file
301
qubespolicy/gtkhelpers.py
Normal file
@ -0,0 +1,301 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2017 boring-stuff <boring-stuff@users.noreply.github.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
|
||||
import qubes
|
||||
import gi, os
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk, GdkPixbuf, GObject, GLib
|
||||
from . qubesutils import sanitize_domain_name
|
||||
|
||||
glade_directory = os.path.join(os.path.dirname(__file__), "glade")
|
||||
|
||||
class GtkIconGetter:
|
||||
def __init__(self, size):
|
||||
self._icons = {}
|
||||
self._size = size
|
||||
self._theme = Gtk.IconTheme.get_default()
|
||||
|
||||
def get_icon(self, name):
|
||||
if name not in self._icons:
|
||||
try:
|
||||
icon = self._theme.load_icon(name, self._size, 0)
|
||||
except GLib.Error:
|
||||
icon = self._theme.load_icon("gnome-foot", self._size, 0)
|
||||
|
||||
self._icons[name] = icon
|
||||
|
||||
return self._icons[name]
|
||||
|
||||
class VMListModeler:
|
||||
def __init__(self):
|
||||
self._icon_getter = GtkIconGetter(16)
|
||||
|
||||
self._entries = {}
|
||||
self._create_entries()
|
||||
|
||||
|
||||
def _get_icon(self, vm):
|
||||
return self._icon_getter.get_icon(vm.label.icon)
|
||||
|
||||
def _get_list(self):
|
||||
collection = qubes.QubesVmCollection()
|
||||
try:
|
||||
collection.lock_db_for_reading()
|
||||
|
||||
collection.load()
|
||||
|
||||
return [vm for vm in collection.values()]
|
||||
finally:
|
||||
collection.unlock_db()
|
||||
|
||||
def _create_entries(self):
|
||||
for vm in self._get_list():
|
||||
sanitize_domain_name(vm.name, assert_sanitized = True)
|
||||
|
||||
icon = self._get_icon(vm)
|
||||
|
||||
self._entries[vm.name] = {'qid': vm.qid,
|
||||
'icon': icon,
|
||||
'vm': vm}
|
||||
|
||||
|
||||
def _get_valid_qube_name(self, combo, entry_box, exclusions):
|
||||
name = None
|
||||
|
||||
if combo and combo.get_active_id():
|
||||
selected = combo.get_active_id()
|
||||
|
||||
if selected in self._entries and selected not in exclusions:
|
||||
name = selected
|
||||
|
||||
if not name and entry_box:
|
||||
typed = entry_box.get_text()
|
||||
|
||||
if typed in self._entries and typed not in exclusions:
|
||||
name = typed
|
||||
|
||||
return name
|
||||
|
||||
def _combo_change(self, selection_trigger, combo, entry_box, exclusions):
|
||||
data = None
|
||||
name = self._get_valid_qube_name(combo, entry_box, exclusions)
|
||||
|
||||
if name:
|
||||
entry = self._entries[name]
|
||||
|
||||
data = (entry['qid'], name)
|
||||
|
||||
if entry_box:
|
||||
entry_box.set_icon_from_pixbuf(
|
||||
Gtk.EntryIconPosition.PRIMARY, entry['icon'])
|
||||
else:
|
||||
if entry_box:
|
||||
entry_box.set_icon_from_stock(
|
||||
Gtk.EntryIconPosition.PRIMARY, "gtk-find")
|
||||
|
||||
if selection_trigger:
|
||||
selection_trigger(data)
|
||||
|
||||
def _entry_activate(self, activation_trigger, combo, entry_box, exclusions):
|
||||
name = self._get_valid_qube_name(combo, entry_box, exclusions)
|
||||
|
||||
if name:
|
||||
activation_trigger(entry_box)
|
||||
|
||||
def apply_model(self, destination_object, vm_filter_list = None,
|
||||
selection_trigger = None, activation_trigger = None):
|
||||
if isinstance(destination_object, Gtk.ComboBox):
|
||||
list_store = Gtk.ListStore(int, str, GdkPixbuf.Pixbuf)
|
||||
|
||||
exclusions = []
|
||||
for vm_name in sorted(self._entries.iterkeys()):
|
||||
entry = self._entries[vm_name]
|
||||
|
||||
matches = True
|
||||
|
||||
if vm_filter_list:
|
||||
for vm_filter in vm_filter_list:
|
||||
if not vm_filter.matches(entry['vm']):
|
||||
matches = False
|
||||
break
|
||||
|
||||
if matches:
|
||||
list_store.append([entry['qid'], vm_name, entry['icon']])
|
||||
else:
|
||||
exclusions += [vm_name]
|
||||
|
||||
destination_object.set_model(list_store)
|
||||
destination_object.set_id_column(1)
|
||||
|
||||
icon_column = Gtk.CellRendererPixbuf()
|
||||
destination_object.pack_start(icon_column, False)
|
||||
destination_object.add_attribute(icon_column, "pixbuf", 2)
|
||||
destination_object.set_entry_text_column(1)
|
||||
|
||||
if destination_object.get_has_entry():
|
||||
entry_box = destination_object.get_child()
|
||||
|
||||
area = Gtk.CellAreaBox()
|
||||
area.pack_start(icon_column, False, False, False)
|
||||
area.add_attribute(icon_column, "pixbuf", 2)
|
||||
|
||||
completion = Gtk.EntryCompletion.new_with_area(area)
|
||||
completion.set_inline_selection(True)
|
||||
completion.set_inline_completion(True)
|
||||
completion.set_popup_completion(True)
|
||||
completion.set_popup_single_match(False)
|
||||
completion.set_model(list_store)
|
||||
completion.set_text_column(1)
|
||||
|
||||
entry_box.set_completion(completion)
|
||||
if activation_trigger:
|
||||
entry_box.connect("activate",
|
||||
lambda entry: self._entry_activate(
|
||||
activation_trigger,
|
||||
destination_object,
|
||||
entry,
|
||||
exclusions))
|
||||
|
||||
# A Combo with an entry has a text column already
|
||||
text_column = destination_object.get_cells()[0]
|
||||
destination_object.reorder(text_column, 1)
|
||||
else:
|
||||
entry_box = None
|
||||
|
||||
text_column = Gtk.CellRendererText()
|
||||
destination_object.pack_start(text_column, False)
|
||||
destination_object.add_attribute(text_column, "text", 1)
|
||||
|
||||
changed_function = lambda combo: self._combo_change(
|
||||
selection_trigger,
|
||||
combo,
|
||||
entry_box,
|
||||
exclusions)
|
||||
|
||||
destination_object.connect("changed", changed_function)
|
||||
changed_function(destination_object)
|
||||
|
||||
else:
|
||||
raise TypeError(
|
||||
"Only expecting Gtk.ComboBox objects to want our model.")
|
||||
|
||||
def apply_icon(self, entry, qube_name):
|
||||
if isinstance(entry, Gtk.Entry):
|
||||
if qube_name in self._entries:
|
||||
entry.set_icon_from_pixbuf(
|
||||
Gtk.EntryIconPosition.PRIMARY,
|
||||
self._entries[qube_name]['icon'])
|
||||
else:
|
||||
raise ValueError("The specified source qube does not exist!")
|
||||
else:
|
||||
raise TypeError(
|
||||
"Only expecting Gtk.Entry objects to want our icon.")
|
||||
|
||||
class NameBlacklistFilter:
|
||||
def __init__(self, avoid_names_list):
|
||||
self._avoid_names_list = avoid_names_list
|
||||
|
||||
def matches(self, vm):
|
||||
return vm.name not in self._avoid_names_list
|
||||
|
||||
class NameWhitelistFilter:
|
||||
def __init__(self, allowed_names_list):
|
||||
self._allowed_names_list = allowed_names_list
|
||||
|
||||
def matches(self, vm):
|
||||
return vm.name in self._allowed_names_list
|
||||
|
||||
class GtkOneTimerHelper:
|
||||
def __init__(self, wait_seconds):
|
||||
self._wait_seconds = wait_seconds
|
||||
self._current_timer_id = 0
|
||||
self._timer_completed = False
|
||||
|
||||
def _invalidate_timer_completed(self):
|
||||
self._timer_completed = False
|
||||
|
||||
def _invalidate_current_timer(self):
|
||||
self._current_timer_id += 1
|
||||
|
||||
def _timer_check_run(self, timer_id):
|
||||
if self._current_timer_id == timer_id:
|
||||
self._timer_run(timer_id)
|
||||
self._timer_completed = True
|
||||
else:
|
||||
pass
|
||||
|
||||
def _timer_run(self, timer_id):
|
||||
raise NotImplementedError("Not yet implemented")
|
||||
|
||||
def _timer_schedule(self):
|
||||
self._invalidate_current_timer()
|
||||
GObject.timeout_add(int(round(self._wait_seconds * 1000)),
|
||||
self._timer_check_run,
|
||||
self._current_timer_id)
|
||||
|
||||
def _timer_has_completed(self):
|
||||
return self._timer_completed
|
||||
|
||||
class FocusStealingHelper(GtkOneTimerHelper):
|
||||
def __init__(self, window, target_button, wait_seconds = 1):
|
||||
GtkOneTimerHelper.__init__(self, wait_seconds)
|
||||
self._window = window
|
||||
self._target_button = target_button
|
||||
|
||||
self._window.connect("window-state-event", self._window_state_event)
|
||||
|
||||
self._target_sensitivity = False
|
||||
self._target_button.set_sensitive(self._target_sensitivity)
|
||||
|
||||
def _window_changed_focus(self, window_is_focused):
|
||||
self._target_button.set_sensitive(False)
|
||||
self._invalidate_timer_completed()
|
||||
|
||||
if window_is_focused:
|
||||
self._timer_schedule()
|
||||
else:
|
||||
self._invalidate_current_timer()
|
||||
|
||||
def _window_state_event(self, window, event):
|
||||
assert window == self._window, \
|
||||
'Window state callback called with wrong window'
|
||||
|
||||
changed_focus = event.changed_mask & Gdk.WindowState.FOCUSED
|
||||
window_focus = event.new_window_state & Gdk.WindowState.FOCUSED
|
||||
|
||||
if changed_focus:
|
||||
self._window_changed_focus(window_focus != 0)
|
||||
|
||||
# Propagate event further
|
||||
return False
|
||||
|
||||
def _timer_run(self, timer_id):
|
||||
self._target_button.set_sensitive(self._target_sensitivity)
|
||||
|
||||
def request_sensitivity(self, sensitivity):
|
||||
if self._timer_has_completed() or not sensitivity:
|
||||
self._target_button.set_sensitive(sensitivity)
|
||||
|
||||
self._target_sensitivity = sensitivity
|
||||
|
||||
def can_perform_action(self):
|
||||
return self._timer_has_completed()
|
||||
|
210
qubespolicy/rpcconfirmation.py
Normal file
210
qubespolicy/rpcconfirmation.py
Normal file
@ -0,0 +1,210 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2017 boring-stuff <boring-stuff@users.noreply.github.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
|
||||
from . gtkhelpers import VMListModeler, FocusStealingHelper, glade_directory
|
||||
from . qubesutils import sanitize_domain_name, sanitize_service_name
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
import os
|
||||
|
||||
class RPCConfirmationWindow():
|
||||
_source_file = os.path.join(glade_directory, "RPCConfirmationWindow.glade")
|
||||
_source_id = { 'window': "RPCConfirmationWindow",
|
||||
'ok': "okButton",
|
||||
'cancel': "cancelButton",
|
||||
'source': "sourceEntry",
|
||||
'rpc_label' : "rpcLabel",
|
||||
'target': "TargetCombo",
|
||||
'error_bar': "ErrorBar",
|
||||
'error_message': "ErrorMessage",
|
||||
}
|
||||
|
||||
def _clicked_ok(self, source):
|
||||
assert source != None, \
|
||||
'Called the clicked ok callback from no source object'
|
||||
|
||||
if self._can_perform_action():
|
||||
self._confirmed = True
|
||||
self._close()
|
||||
|
||||
def _clicked_cancel(self, button):
|
||||
assert button == self._rpc_cancel_button, \
|
||||
'Called the clicked cancel callback through the wrong button'
|
||||
|
||||
if self._can_perform_action():
|
||||
self._confirmed = False
|
||||
self._close()
|
||||
|
||||
def _key_pressed(self, window, key):
|
||||
assert window == self._rpc_window, \
|
||||
'Key pressed callback called with wrong window'
|
||||
|
||||
if self._can_perform_action():
|
||||
if key.keyval == Gdk.KEY_Escape:
|
||||
self._confirmed = False
|
||||
self._close()
|
||||
|
||||
def _update_ok_button_sensitivity(self, data):
|
||||
valid = (data != None)
|
||||
|
||||
if valid:
|
||||
(self._target_qid, self._target_name) = data
|
||||
else:
|
||||
self._target_qid = None
|
||||
self._target_name = None
|
||||
|
||||
self._focus_helper.request_sensitivity(valid)
|
||||
|
||||
def _show_error(self, error_message):
|
||||
self._error_message.set_text(error_message)
|
||||
self._error_bar.set_visible(True)
|
||||
|
||||
def _close_error(self, error_bar, response):
|
||||
assert error_bar == self._error_bar, \
|
||||
'Closed the error bar with the wrong error bar as parameter'
|
||||
assert response != None, 'Closed the error bar with None as a response'
|
||||
|
||||
self._error_bar.set_visible(False)
|
||||
|
||||
def _set_initial_target(self, source, target):
|
||||
if target != None:
|
||||
if target == source:
|
||||
self._show_error(
|
||||
"Source and target domains must not be the same.")
|
||||
else:
|
||||
model = self._rpc_combo_box.get_model()
|
||||
|
||||
found = False
|
||||
for item in model:
|
||||
if item[1] == target:
|
||||
found = True
|
||||
|
||||
self._rpc_combo_box.set_active_iter(
|
||||
model.get_iter(item.path))
|
||||
|
||||
break
|
||||
|
||||
if not found:
|
||||
self._show_error("Domain '%s' doesn't exist." % target)
|
||||
|
||||
def _can_perform_action(self):
|
||||
return self._focus_helper.can_perform_action()
|
||||
|
||||
def _escape_and_format_rpc_text(self, rpc_operation):
|
||||
escaped = GLib.markup_escape_text(rpc_operation)
|
||||
|
||||
partitioned = escaped.partition('.')
|
||||
formatted = partitioned[0] + partitioned[1]
|
||||
|
||||
if len(partitioned[2]) > 0:
|
||||
formatted += "<b>" + partitioned[2] + "</b>"
|
||||
else:
|
||||
formatted = "<b>" + formatted + "</b>"
|
||||
|
||||
return formatted
|
||||
|
||||
def _connect_events(self):
|
||||
self._rpc_window.connect("key-press-event",self._key_pressed)
|
||||
self._rpc_ok_button.connect("clicked", self._clicked_ok)
|
||||
self._rpc_cancel_button.connect("clicked", self._clicked_cancel)
|
||||
|
||||
self._error_bar.connect("response", self._close_error)
|
||||
|
||||
def __init__(self, source, rpc_operation, name_whitelist, target = None):
|
||||
sanitize_domain_name(source, assert_sanitized = True)
|
||||
sanitize_service_name(source, assert_sanitized = True)
|
||||
|
||||
self._gtk_builder = Gtk.Builder()
|
||||
self._gtk_builder.add_from_file(self._source_file)
|
||||
self._rpc_window = self._gtk_builder.get_object(
|
||||
self._source_id['window'])
|
||||
self._rpc_ok_button = self._gtk_builder.get_object(
|
||||
self._source_id['ok'])
|
||||
self._rpc_cancel_button = self._gtk_builder.get_object(
|
||||
self._source_id['cancel'])
|
||||
self._rpc_label = self._gtk_builder.get_object(
|
||||
self._source_id['rpc_label'])
|
||||
self._source_entry = self._gtk_builder.get_object(
|
||||
self._source_id['source'])
|
||||
self._rpc_combo_box = self._gtk_builder.get_object(
|
||||
self._source_id['target'])
|
||||
self._error_bar = self._gtk_builder.get_object(
|
||||
self._source_id['error_bar'])
|
||||
self._error_message = self._gtk_builder.get_object(
|
||||
self._source_id['error_message'])
|
||||
self._target_qid = None
|
||||
self._target_name = None
|
||||
|
||||
self._focus_helper = self._new_focus_stealing_helper()
|
||||
|
||||
self._rpc_label.set_markup(
|
||||
self._escape_and_format_rpc_text(rpc_operation))
|
||||
|
||||
list_modeler = self._new_VM_list_modeler()
|
||||
|
||||
domain_filters = [VMListModeler.NameWhitelistFilter(name_whitelist)]
|
||||
|
||||
list_modeler.apply_model(self._rpc_combo_box, domain_filters,
|
||||
selection_trigger = self._update_ok_button_sensitivity,
|
||||
activation_trigger = self._clicked_ok )
|
||||
|
||||
self._source_entry.set_text(source)
|
||||
list_modeler.apply_icon(self._source_entry, source)
|
||||
|
||||
self._confirmed = None
|
||||
|
||||
self._set_initial_target(source, target)
|
||||
|
||||
self._connect_events()
|
||||
|
||||
def _close(self):
|
||||
self._rpc_window.close()
|
||||
|
||||
def _show(self):
|
||||
self._rpc_window.set_keep_above(True)
|
||||
self._rpc_window.connect("delete-event", Gtk.main_quit)
|
||||
self._rpc_window.show_all()
|
||||
|
||||
Gtk.main()
|
||||
|
||||
def _new_VM_list_modeler(self):
|
||||
return VMListModeler()
|
||||
|
||||
def _new_focus_stealing_helper(self):
|
||||
return FocusStealingHelper(
|
||||
self._rpc_window,
|
||||
self._rpc_ok_button,
|
||||
1)
|
||||
|
||||
def confirm_rpc(self):
|
||||
self._show()
|
||||
|
||||
if self._confirmed:
|
||||
return { 'name': self._target_name, 'qid': self._target_qid,
|
||||
'parameters': {} }
|
||||
else:
|
||||
return False
|
||||
|
||||
def confirm_rpc(source, rpc_operation, name_whitelist, target = None):
|
||||
window = RPCConfirmationWindow(source, rpc_operation, name_whitelist,
|
||||
target)
|
||||
|
||||
return window.confirm_rpc()
|
||||
|
444
qubespolicy/tests/gtkhelpers.py
Executable file
444
qubespolicy/tests/gtkhelpers.py
Executable file
@ -0,0 +1,444 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2017 boring-stuff <boring-stuff@users.noreply.github.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
|
||||
import unittest, time
|
||||
from core.gtkhelpers import VMListModeler, GtkOneTimerHelper, FocusStealingHelper, Gtk
|
||||
|
||||
class VMListModelerMock(VMListModeler):
|
||||
def __init__(self):
|
||||
VMListModeler.__init__(self)
|
||||
|
||||
def _get_list(self):
|
||||
return [ MockVm(0, "dom0", "black"),
|
||||
MockVm(2, "test-red1", "red"),
|
||||
MockVm(4, "test-red2", "red"),
|
||||
MockVm(7, "test-red3", "red"),
|
||||
MockVm(8, "test-source", "green"),
|
||||
MockVm(10, "test-target", "orange"),
|
||||
MockVm(15, "test-disp6", "red", True) ]
|
||||
|
||||
@staticmethod
|
||||
def get_name_whitelist():
|
||||
return [ "test-red1", "test-red2", "test-red3",
|
||||
"test-target", "test-disp6" ]
|
||||
|
||||
class MockVmLabel:
|
||||
def __init__(self, index, color, name, dispvm = False):
|
||||
self.index = index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.dispvm = dispvm
|
||||
self.icon = "gnome-foot"
|
||||
|
||||
class MockVm:
|
||||
def __init__(self, qid, name, color, dispvm = False):
|
||||
self.qid = qid
|
||||
self.name = name
|
||||
self.label = MockVmLabel(qid, 0x000000, color, dispvm)
|
||||
|
||||
class MockComboEntry:
|
||||
def __init__(self, text):
|
||||
self._text = text
|
||||
|
||||
def get_active_id(self):
|
||||
return self._text
|
||||
|
||||
def get_text(self):
|
||||
return self._text
|
||||
|
||||
class GtkTestCase(unittest.TestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
self._smallest_wait = 0.01
|
||||
|
||||
def flush_gtk_events(self, wait_seconds = 0):
|
||||
start = time.time()
|
||||
iterations = 0
|
||||
remaining_wait = wait_seconds
|
||||
time_length = 0
|
||||
|
||||
if wait_seconds < 0:
|
||||
raise ValueError("Only non-negative intervals are allowed.")
|
||||
|
||||
while remaining_wait >= 0:
|
||||
while Gtk.events_pending():
|
||||
Gtk.main_iteration_do(blocking = False)
|
||||
iterations += 1
|
||||
|
||||
time_length = time.time() - start
|
||||
remaining_wait = wait_seconds - time_length
|
||||
|
||||
if (remaining_wait > 0):
|
||||
time.sleep(self._smallest_wait)
|
||||
|
||||
return (iterations, time_length)
|
||||
|
||||
class VMListModelerTest(VMListModelerMock, unittest.TestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
VMListModelerMock.__init__(self)
|
||||
|
||||
def test_entries_gets_loaded(self):
|
||||
self.assertIsNotNone(self._entries)
|
||||
|
||||
def test_valid_qube_name(self):
|
||||
self.apply_model(Gtk.ComboBox())
|
||||
|
||||
for name in [ "test-red1", "test-red2", "test-red3",
|
||||
"test-target", "test-disp6" ]:
|
||||
|
||||
mock = MockComboEntry(name)
|
||||
self.assertEquals(name, self._get_valid_qube_name(mock, mock, []))
|
||||
self.assertEquals(name, self._get_valid_qube_name(None, mock, []))
|
||||
self.assertEquals(name, self._get_valid_qube_name(mock, None, []))
|
||||
self.assertIsNone(self._get_valid_qube_name(None, None, []))
|
||||
|
||||
def test_valid_qube_name_exceptions(self):
|
||||
list_exc = ["test-disp6", "test-red2"]
|
||||
|
||||
self.apply_model(Gtk.ComboBox(),
|
||||
[VMListModeler.NameBlacklistFilter([ list_exc[0], list_exc[1] ]) ])
|
||||
|
||||
for name in list_exc:
|
||||
mock = MockComboEntry(name)
|
||||
self.assertIsNone(self._get_valid_qube_name(mock, mock, list_exc))
|
||||
self.assertIsNone(self._get_valid_qube_name(None, mock, list_exc))
|
||||
self.assertIsNone(self._get_valid_qube_name(mock, None, list_exc))
|
||||
|
||||
def test_invalid_qube_name(self):
|
||||
self.apply_model(Gtk.ComboBox())
|
||||
|
||||
for name in [ "test-nonexistant", None, "", 1 ]:
|
||||
|
||||
mock = MockComboEntry(name)
|
||||
self.assertIsNone(self._get_valid_qube_name(mock, mock, []))
|
||||
self.assertIsNone(self._get_valid_qube_name(None, mock, []))
|
||||
self.assertIsNone(self._get_valid_qube_name(mock, None, []))
|
||||
|
||||
def test_apply_model(self):
|
||||
new_object = Gtk.ComboBox()
|
||||
self.assertIsNone(new_object.get_model())
|
||||
|
||||
self.apply_model(new_object)
|
||||
|
||||
self.assertIsNotNone(new_object.get_model())
|
||||
|
||||
def test_apply_model_with_entry(self):
|
||||
new_object = Gtk.ComboBox.new_with_entry()
|
||||
|
||||
self.assertIsNone(new_object.get_model())
|
||||
|
||||
self.apply_model(new_object)
|
||||
|
||||
self.assertIsNotNone(new_object.get_model())
|
||||
|
||||
def test_apply_model_only_combobox(self):
|
||||
invalid_types = [ 1, "One", u'1', {'1': "one"}, VMListModelerMock()]
|
||||
|
||||
for invalid_type in invalid_types:
|
||||
with self.assertRaises(TypeError):
|
||||
self.apply_model(invalid_type)
|
||||
|
||||
def test_apply_model_blacklist(self):
|
||||
combo = Gtk.ComboBox()
|
||||
|
||||
self.apply_model(combo)
|
||||
self.assertEquals(7, len(combo.get_model()))
|
||||
|
||||
self.apply_model(combo, [ VMListModeler.NameBlacklistFilter([
|
||||
self._entries.keys()[0] ]) ])
|
||||
self.assertEquals(6, len(combo.get_model()))
|
||||
|
||||
self.apply_model(combo, [ VMListModeler.NameBlacklistFilter([
|
||||
self._entries.keys()[0] ]),
|
||||
VMListModeler.NameBlacklistFilter([
|
||||
self._entries.keys()[1] ]) ])
|
||||
self.assertEquals(5, len(combo.get_model()))
|
||||
|
||||
self.apply_model(combo, [ VMListModeler.NameBlacklistFilter([
|
||||
self._entries.keys()[0],
|
||||
self._entries.keys()[1] ]) ])
|
||||
self.assertEquals(5, len(combo.get_model()))
|
||||
|
||||
def test_apply_model_whitelist(self):
|
||||
combo = Gtk.ComboBox()
|
||||
|
||||
self.apply_model(combo)
|
||||
self.assertEquals(7, len(combo.get_model()))
|
||||
|
||||
self.apply_model(combo, [ VMListModeler.NameWhitelistFilter([
|
||||
self._entries.keys()[0] ]) ])
|
||||
self.assertEquals(1, len(combo.get_model()))
|
||||
|
||||
self.apply_model(combo, [ VMListModeler.NameWhitelistFilter([
|
||||
self._entries.keys()[0],
|
||||
self._entries.keys()[1] ]) ])
|
||||
self.assertEquals(2, len(combo.get_model()))
|
||||
|
||||
def test_apply_model_multiple_filters(self):
|
||||
combo = Gtk.ComboBox()
|
||||
|
||||
self.apply_model(combo)
|
||||
self.assertEquals(7, len(combo.get_model()))
|
||||
|
||||
self.apply_model(combo, [ VMListModeler.NameWhitelistFilter([
|
||||
self._entries.keys()[0],
|
||||
self._entries.keys()[1],
|
||||
self._entries.keys()[2],
|
||||
self._entries.keys()[3],
|
||||
self._entries.keys()[4] ]),
|
||||
VMListModeler.NameBlacklistFilter([
|
||||
self._entries.keys()[0],
|
||||
self._entries.keys()[1] ]) ])
|
||||
self.assertEquals(3, len(combo.get_model()))
|
||||
|
||||
def test_apply_icon(self):
|
||||
new_object = Gtk.Entry()
|
||||
|
||||
self.assertIsNone(
|
||||
new_object.get_icon_pixbuf(Gtk.EntryIconPosition.PRIMARY))
|
||||
|
||||
self.apply_icon(new_object, "test-disp6")
|
||||
|
||||
self.assertIsNotNone(
|
||||
new_object.get_icon_pixbuf(Gtk.EntryIconPosition.PRIMARY))
|
||||
|
||||
def test_apply_icon_only_entry(self):
|
||||
invalid_types = [ 1, "One", u'1', {'1': "one"}, Gtk.ComboBox()]
|
||||
|
||||
for invalid_type in invalid_types:
|
||||
with self.assertRaises(TypeError):
|
||||
self.apply_icon(invalid_type, "test-disp6")
|
||||
|
||||
def test_apply_icon_only_existing(self):
|
||||
new_object = Gtk.Entry()
|
||||
|
||||
for name in [ "test-red1", "test-red2", "test-red3",
|
||||
"test-target", "test-disp6" ]:
|
||||
self.apply_icon(new_object, name)
|
||||
|
||||
for name in [ "test-nonexistant", None, "", 1 ]:
|
||||
with self.assertRaises(ValueError):
|
||||
self.apply_icon(new_object, name)
|
||||
|
||||
class GtkOneTimerHelperTest(GtkOneTimerHelper, GtkTestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
GtkTestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
self._test_time = 0.1
|
||||
|
||||
GtkOneTimerHelper.__init__(self, self._test_time)
|
||||
self._run_timers = []
|
||||
|
||||
def _timer_run(self, timer_id):
|
||||
self._run_timers.append(timer_id)
|
||||
|
||||
def test_nothing_runs_automatically(self):
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([], self._run_timers)
|
||||
self.assertEquals(0, self._current_timer_id)
|
||||
self.assertFalse(self._timer_has_completed())
|
||||
|
||||
def test_schedule_one_task(self):
|
||||
self._timer_schedule()
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([1], self._run_timers)
|
||||
self.assertEquals(1, self._current_timer_id)
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
|
||||
def test_invalidate_completed(self):
|
||||
self._timer_schedule()
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([1], self._run_timers)
|
||||
self.assertEquals(1, self._current_timer_id)
|
||||
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
self._invalidate_timer_completed()
|
||||
self.assertFalse(self._timer_has_completed())
|
||||
|
||||
def test_schedule_and_cancel_one_task(self):
|
||||
self._timer_schedule()
|
||||
self._invalidate_current_timer()
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([], self._run_timers)
|
||||
self.assertEquals(2, self._current_timer_id)
|
||||
self.assertFalse(self._timer_has_completed())
|
||||
|
||||
def test_two_tasks(self):
|
||||
self._timer_schedule()
|
||||
self.flush_gtk_events(self._test_time/4)
|
||||
self._timer_schedule()
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([2], self._run_timers)
|
||||
self.assertEquals(2, self._current_timer_id)
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
|
||||
def test_more_tasks(self):
|
||||
num = 0
|
||||
for num in range(1,10):
|
||||
self._timer_schedule()
|
||||
self.flush_gtk_events(self._test_time/4)
|
||||
self.flush_gtk_events(self._test_time*1.75)
|
||||
self.assertEquals([num], self._run_timers)
|
||||
self.assertEquals(num, self._current_timer_id)
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
|
||||
def test_more_tasks_cancel(self):
|
||||
num = 0
|
||||
for num in range(1,10):
|
||||
self._timer_schedule()
|
||||
self.flush_gtk_events(self._test_time/4)
|
||||
self._invalidate_current_timer()
|
||||
self.flush_gtk_events(self._test_time*1.75)
|
||||
self.assertEquals([], self._run_timers)
|
||||
self.assertEquals(num+1, self._current_timer_id)
|
||||
self.assertFalse(self._timer_has_completed())
|
||||
|
||||
def test_subsequent_tasks(self):
|
||||
self._timer_schedule() #1
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([1], self._run_timers)
|
||||
self.assertEquals(1, self._current_timer_id)
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
|
||||
self._timer_schedule() #2
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([1,2], self._run_timers)
|
||||
self.assertEquals(2, self._current_timer_id)
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
|
||||
self._invalidate_timer_completed()
|
||||
self._timer_schedule() #3
|
||||
self._invalidate_current_timer() #4
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([1,2], self._run_timers)
|
||||
self.assertEquals(4, self._current_timer_id)
|
||||
self.assertFalse(self._timer_has_completed())
|
||||
|
||||
self._timer_schedule() #5
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([1,2,5], self._run_timers)
|
||||
self.assertEquals(5, self._current_timer_id)
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
|
||||
class FocusStealingHelperMock(FocusStealingHelper):
|
||||
def simulate_focus(self):
|
||||
self._window_changed_focus(True)
|
||||
|
||||
class FocusStealingHelperTest(FocusStealingHelperMock, GtkTestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
GtkTestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
self._test_time = 0.1
|
||||
self._test_button = Gtk.Button()
|
||||
self._test_window = Gtk.Window()
|
||||
|
||||
FocusStealingHelperMock.__init__(self, self._test_window,
|
||||
self._test_button, self._test_time)
|
||||
|
||||
def test_nothing_runs_automatically(self):
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
def test_nothing_runs_automatically_with_request(self):
|
||||
self.request_sensitivity(True)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
def _simulate_focus(self, focused):
|
||||
self._window_changed_focus(focused)
|
||||
|
||||
def test_focus_with_request(self):
|
||||
self.request_sensitivity(True)
|
||||
self._simulate_focus(True)
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertTrue(self._test_button.get_sensitive())
|
||||
|
||||
def test_focus_with_late_request(self):
|
||||
self._simulate_focus(True)
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
self.request_sensitivity(True)
|
||||
self.assertTrue(self._test_button.get_sensitive())
|
||||
|
||||
def test_immediate_defocus(self):
|
||||
self.request_sensitivity(True)
|
||||
self._simulate_focus(True)
|
||||
self._simulate_focus(False)
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
def test_focus_then_unfocus(self):
|
||||
self.request_sensitivity(True)
|
||||
self._simulate_focus(True)
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertTrue(self._test_button.get_sensitive())
|
||||
|
||||
self._simulate_focus(False)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
def test_focus_cycle(self):
|
||||
self.request_sensitivity(True)
|
||||
|
||||
self._simulate_focus(True)
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertTrue(self._test_button.get_sensitive())
|
||||
|
||||
self._simulate_focus(False)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
self._simulate_focus(True)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertTrue(self._test_button.get_sensitive())
|
||||
|
||||
self.request_sensitivity(False)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
self._simulate_focus(False)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
|
||||
self._simulate_focus(True)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
if __name__=='__main__':
|
||||
unittest.main()
|
334
qubespolicy/tests/rpcconfirmation.py
Executable file
334
qubespolicy/tests/rpcconfirmation.py
Executable file
@ -0,0 +1,334 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2017 boring-stuff <boring-stuff@users.noreply.github.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
|
||||
import unittest, sys
|
||||
from core.rpcconfirmation import RPCConfirmationWindow
|
||||
from gtkhelpers import VMListModelerMock, GtkTestCase, FocusStealingHelperMock
|
||||
|
||||
class MockRPCConfirmationWindow(RPCConfirmationWindow):
|
||||
def _new_VM_list_modeler(self):
|
||||
return VMListModelerMock()
|
||||
|
||||
def _new_focus_stealing_helper(self):
|
||||
return FocusStealingHelperMock(
|
||||
self._rpc_window,
|
||||
self._rpc_ok_button,
|
||||
self._focus_stealing_seconds)
|
||||
|
||||
def __init__(self, source, rpc_operation,
|
||||
name_whitelist = VMListModelerMock.get_name_whitelist(),
|
||||
target = None, focus_stealing_seconds = 1):
|
||||
self._focus_stealing_seconds = focus_stealing_seconds
|
||||
|
||||
RPCConfirmationWindow.__init__(self, source, rpc_operation,
|
||||
name_whitelist, target)
|
||||
|
||||
def is_error_visible(self):
|
||||
return self._error_bar.get_visible()
|
||||
|
||||
def get_shown_domains(self):
|
||||
model = self._rpc_combo_box.get_model()
|
||||
model_iter = model.get_iter_first()
|
||||
domains = []
|
||||
|
||||
while model_iter != None:
|
||||
domain_name = model.get_value(model_iter, 1)
|
||||
|
||||
domains = domains + [domain_name]
|
||||
|
||||
model_iter = model.iter_next(model_iter)
|
||||
|
||||
return domains
|
||||
|
||||
class RPCConfirmationWindowTestBase(MockRPCConfirmationWindow, GtkTestCase):
|
||||
def __init__(self, test_method, source_name = "test-source",
|
||||
rpc_operation = "test.Operation",
|
||||
name_whitelist = VMListModelerMock.get_name_whitelist(),
|
||||
target_name = None):
|
||||
GtkTestCase.__init__(self, test_method)
|
||||
self.test_source_name = source_name
|
||||
self.test_rpc_operation = rpc_operation
|
||||
self.test_target_name = target_name
|
||||
|
||||
self._test_time = 0.1
|
||||
|
||||
self.test_called_close = False
|
||||
self.test_called_show = False
|
||||
|
||||
self.test_clicked_ok = False
|
||||
self.test_clicked_cancel = False
|
||||
|
||||
MockRPCConfirmationWindow.__init__(self,
|
||||
self.test_source_name,
|
||||
self.test_rpc_operation,
|
||||
name_whitelist,
|
||||
self.test_target_name,
|
||||
focus_stealing_seconds = self._test_time)
|
||||
|
||||
def _can_perform_action(self):
|
||||
return True
|
||||
|
||||
def _close(self):
|
||||
self.test_called_close = True
|
||||
|
||||
def _show(self):
|
||||
self.test_called_show = True
|
||||
|
||||
def _clicked_ok(self, button):
|
||||
MockRPCConfirmationWindow._clicked_ok(self, button)
|
||||
self.test_clicked_ok = True
|
||||
|
||||
def _clicked_cancel(self, button):
|
||||
MockRPCConfirmationWindow._clicked_cancel(self, button)
|
||||
self.test_clicked_cancel = True
|
||||
|
||||
def test_has_linked_the_fields(self):
|
||||
self.assertIsNotNone(self._rpc_window)
|
||||
self.assertIsNotNone(self._rpc_ok_button)
|
||||
self.assertIsNotNone(self._rpc_cancel_button)
|
||||
self.assertIsNotNone(self._rpc_label)
|
||||
self.assertIsNotNone(self._source_entry)
|
||||
self.assertIsNotNone(self._rpc_combo_box)
|
||||
self.assertIsNotNone(self._error_bar)
|
||||
self.assertIsNotNone(self._error_message)
|
||||
|
||||
def test_is_showing_source(self):
|
||||
self.assertTrue(self.test_source_name in
|
||||
self._source_entry.get_text())
|
||||
|
||||
def test_is_showing_operation(self):
|
||||
self.assertTrue(self.test_rpc_operation in
|
||||
self._rpc_label.get_text())
|
||||
|
||||
def test_escape_and_format_rpc_text(self):
|
||||
self.assertEquals("qubes.<b>Test</b>",
|
||||
self._escape_and_format_rpc_text("qubes.Test"))
|
||||
self.assertEquals("custom.<b>Domain</b>",
|
||||
self._escape_and_format_rpc_text("custom.Domain"))
|
||||
self.assertEquals("<b>nodomain</b>",
|
||||
self._escape_and_format_rpc_text("nodomain"))
|
||||
self.assertEquals("domain.<b>Sub.Operation</b>",
|
||||
self._escape_and_format_rpc_text("domain.Sub.Operation"))
|
||||
self.assertEquals("<b></b>",
|
||||
self._escape_and_format_rpc_text(""))
|
||||
self.assertEquals("<b>.</b>",
|
||||
self._escape_and_format_rpc_text("."))
|
||||
self.assertEquals("inject.<b><script></b>",
|
||||
self._escape_and_format_rpc_text("inject.<script>"))
|
||||
self.assertEquals("<script>.<b>inject</b>",
|
||||
self._escape_and_format_rpc_text("<script>.inject"))
|
||||
|
||||
def test_lifecycle_open_select_ok(self):
|
||||
self._lifecycle_start(select_target = True)
|
||||
self._lifecycle_click(click_type = "ok")
|
||||
|
||||
def test_lifecycle_open_select_cancel(self):
|
||||
self._lifecycle_start(select_target = True)
|
||||
self._lifecycle_click(click_type = "cancel")
|
||||
|
||||
def test_lifecycle_open_select_exit(self):
|
||||
self._lifecycle_start(select_target = True)
|
||||
self._lifecycle_click(click_type = "exit")
|
||||
|
||||
def test_lifecycle_open_cancel(self):
|
||||
self._lifecycle_start(select_target = False)
|
||||
self._lifecycle_click(click_type = "cancel")
|
||||
|
||||
def test_lifecycle_open_exit(self):
|
||||
self._lifecycle_start(select_target = False)
|
||||
self._lifecycle_click(click_type = "exit")
|
||||
|
||||
def _lifecycle_click(self, click_type):
|
||||
if click_type == "ok":
|
||||
self._rpc_ok_button.clicked()
|
||||
|
||||
self.assertTrue(self.test_clicked_ok)
|
||||
self.assertFalse(self.test_clicked_cancel)
|
||||
self.assertTrue(self._confirmed)
|
||||
self.assertIsNotNone(self._target_qid)
|
||||
self.assertIsNotNone(self._target_name)
|
||||
elif click_type == "cancel":
|
||||
self._rpc_cancel_button.clicked()
|
||||
|
||||
self.assertFalse(self.test_clicked_ok)
|
||||
self.assertTrue(self.test_clicked_cancel)
|
||||
self.assertFalse(self._confirmed)
|
||||
elif click_type == "exit":
|
||||
self._close()
|
||||
|
||||
self.assertFalse(self.test_clicked_ok)
|
||||
self.assertFalse(self.test_clicked_cancel)
|
||||
self.assertIsNone(self._confirmed)
|
||||
|
||||
self.assertTrue(self.test_called_close)
|
||||
|
||||
|
||||
def _lifecycle_start(self, select_target):
|
||||
self.assertFalse(self.test_called_close)
|
||||
self.assertFalse(self.test_called_show)
|
||||
|
||||
self.assert_initial_state(False)
|
||||
self.assertTrue(isinstance(self._focus_helper, FocusStealingHelperMock))
|
||||
|
||||
# Need the following because of pylint's complaints
|
||||
if isinstance(self._focus_helper, FocusStealingHelperMock):
|
||||
FocusStealingHelperMock.simulate_focus(self._focus_helper)
|
||||
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assert_initial_state(True)
|
||||
|
||||
try:
|
||||
# We expect the call to exit immediately, since no window is opened
|
||||
self.confirm_rpc()
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
self.assertFalse(self.test_called_close)
|
||||
self.assertTrue(self.test_called_show)
|
||||
|
||||
self.assert_initial_state(True)
|
||||
|
||||
if select_target:
|
||||
self._rpc_combo_box.set_active(1)
|
||||
|
||||
self.assertTrue(self._rpc_ok_button.get_sensitive())
|
||||
|
||||
self.assertIsNotNone(self._target_qid)
|
||||
self.assertIsNotNone(self._target_name)
|
||||
|
||||
self.assertFalse(self.test_called_close)
|
||||
self.assertTrue(self.test_called_show)
|
||||
self.assertFalse(self.test_clicked_ok)
|
||||
self.assertFalse(self.test_clicked_cancel)
|
||||
self.assertFalse(self._confirmed)
|
||||
|
||||
def assert_initial_state(self, after_focus_timer):
|
||||
self.assertIsNone(self._target_qid)
|
||||
self.assertIsNone(self._target_name)
|
||||
self.assertFalse(self.test_clicked_ok)
|
||||
self.assertFalse(self.test_clicked_cancel)
|
||||
self.assertFalse(self._confirmed)
|
||||
self.assertFalse(self._rpc_ok_button.get_sensitive())
|
||||
self.assertFalse(self._error_bar.get_visible())
|
||||
|
||||
if after_focus_timer:
|
||||
self.assertTrue(self._focus_helper.can_perform_action())
|
||||
else:
|
||||
self.assertFalse(self._focus_helper.can_perform_action())
|
||||
|
||||
class RPCConfirmationWindowTestWithTarget(RPCConfirmationWindowTestBase):
|
||||
def __init__(self, test_method):
|
||||
RPCConfirmationWindowTestBase.__init__(self, test_method,
|
||||
source_name = "test-source", rpc_operation = "test.Operation",
|
||||
target_name = "test-target")
|
||||
|
||||
def test_lifecycle_open_ok(self):
|
||||
self._lifecycle_start(select_target = False)
|
||||
self._lifecycle_click(click_type = "ok")
|
||||
|
||||
def assert_initial_state(self, after_focus_timer):
|
||||
self.assertIsNotNone(self._target_qid)
|
||||
self.assertIsNotNone(self._target_name)
|
||||
self.assertFalse(self.test_clicked_ok)
|
||||
self.assertFalse(self.test_clicked_cancel)
|
||||
self.assertFalse(self._confirmed)
|
||||
if after_focus_timer:
|
||||
self.assertTrue(self._rpc_ok_button.get_sensitive())
|
||||
self.assertTrue(self._focus_helper.can_perform_action())
|
||||
else:
|
||||
self.assertFalse(self._rpc_ok_button.get_sensitive())
|
||||
self.assertFalse(self._focus_helper.can_perform_action())
|
||||
|
||||
def _lifecycle_click(self, click_type):
|
||||
RPCConfirmationWindowTestBase._lifecycle_click(self, click_type)
|
||||
self.assertIsNotNone(self._target_qid)
|
||||
self.assertIsNotNone(self._target_name)
|
||||
|
||||
class RPCConfirmationWindowTestWithTargetInvalid(unittest.TestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
def test_unknown(self):
|
||||
self.assert_raises_error(True, "test-source", "test-wrong-target")
|
||||
|
||||
def test_empty(self):
|
||||
self.assert_raises_error(True, "test-source", "")
|
||||
|
||||
def test_equals_source(self):
|
||||
self.assert_raises_error(True, "test-source", "test-source")
|
||||
|
||||
def assert_raises_error(self, expect, source, target):
|
||||
rpcWindow = MockRPCConfirmationWindow(source, "test.Operation",
|
||||
target=target)
|
||||
self.assertEquals(expect, rpcWindow.is_error_visible())
|
||||
|
||||
class RPCConfirmationWindowTestWhitelist(unittest.TestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
def test_no_domains(self):
|
||||
self._assert_whitelist([],[])
|
||||
|
||||
def test_all_red_domains(self):
|
||||
self._assert_whitelist(["test-red1", "test-red2", "test-red3"],
|
||||
["test-red1", "test-red2", "test-red3"])
|
||||
|
||||
def test_all_red_domains_plus_nonexistent(self):
|
||||
self._assert_whitelist(["test-red1", "test-red2", "test-red3",
|
||||
"test-blue1", "test-blue2", "test-blue3"],
|
||||
["test-red1", "test-red2", "test-red3"])
|
||||
|
||||
def test_all_allowed_domains(self):
|
||||
self._assert_whitelist(["test-red1", "test-red2", "test-red3",
|
||||
"test-target", "test-disp6", "test-source",
|
||||
"dom0"], ["test-red1", "test-red2", "test-red3",
|
||||
"test-target", "test-disp6", "test-source",
|
||||
"dom0"])
|
||||
|
||||
def _assert_whitelist(self, whitelist, expected):
|
||||
rpcWindow = MockRPCConfirmationWindow("test-source", "test.Operation",
|
||||
whitelist)
|
||||
|
||||
domains = rpcWindow.get_shown_domains()
|
||||
|
||||
for domain in expected:
|
||||
self.assertTrue(domain in domains)
|
||||
|
||||
self.assertEquals(len(expected), len(domains))
|
||||
|
||||
if __name__=='__main__':
|
||||
test = False
|
||||
window = False
|
||||
|
||||
if len(sys.argv) == 1 or sys.argv[1] == '-t':
|
||||
test = True
|
||||
elif sys.argv[1] == '-w':
|
||||
window = True
|
||||
else:
|
||||
print "Usage: " + __file__ + " [-t|-w]"
|
||||
|
||||
if window:
|
||||
print MockRPCConfirmationWindow("test-source",
|
||||
"qubes.Filecopy",
|
||||
VMListModelerMock.get_name_whitelist(),
|
||||
"test-red1").confirm_rpc()
|
||||
elif test:
|
||||
unittest.main(argv = [sys.argv[0]])
|
56
qubespolicy/utils.py
Normal file
56
qubespolicy/utils.py
Normal file
@ -0,0 +1,56 @@
|
||||
# -*- encoding: utf8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2017 boring-stuff <boring-stuff@users.noreply.github.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
def _sanitize_char(input_char, extra_allowed_characters):
|
||||
input_char_ord = ord(input_char)
|
||||
|
||||
if (input_char_ord >= ord('a') and input_char_ord <= ord('z')) \
|
||||
or (input_char_ord >= ord('A') and input_char_ord <= ord('Z')) \
|
||||
or (input_char_ord >= ord('0') and input_char_ord <= ord('9')) \
|
||||
or (input_char in ['$', '_', '-', '.']) \
|
||||
or (extra_allowed_characters != None
|
||||
and input_char in extra_allowed_characters):
|
||||
result = input_char
|
||||
else:
|
||||
result = '_'
|
||||
|
||||
return result
|
||||
|
||||
# This function needs to be synchronized with qrexec-daemon.c's sanitize_name()
|
||||
# from the qubes-core-admin-linux repository.
|
||||
#
|
||||
# See https://github.com/QubesOS/qubes-core-admin-linux/blob/4f0878ccbf8a95f8264b54d2b6f4dc433ca0793a/qrexec/qrexec-daemon.c#L627-L646
|
||||
#
|
||||
def _sanitize_name(input_string, extra_allowed_characters, assert_sanitized):
|
||||
result = ''.join(_sanitize_char(character, extra_allowed_characters) \
|
||||
for character in input_string)
|
||||
|
||||
if assert_sanitized:
|
||||
assert input_string == result, \
|
||||
'Input string was expected to be sanitized, but was not.'
|
||||
else:
|
||||
return result
|
||||
|
||||
def sanitize_domain_name(input_string, assert_sanitized = False):
|
||||
return _sanitize_name(input_string, None, assert_sanitized)
|
||||
|
||||
def sanitize_service_name(input_string, assert_sanitized = False):
|
||||
return _sanitize_name(input_string, {'+'}, assert_sanitized)
|
||||
|
Loading…
Reference in New Issue
Block a user