Completed
Push — master ( f4924a...c72d79 )
by Matěj
16s queued 13s
created

GtkActionList.fire()   A

Complexity

Conditions 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
# 
2
# Copyright (C) 2013  Red Hat, Inc.
3
#
4
# This copyrighted material is made available to anyone wishing to use,
5
# modify, copy, or redistribute it subject to the terms and conditions of
6
# the GNU General Public License v.2, or (at your option) any later version.
7
# This program is distributed in the hope that it will be useful, but WITHOUT
8
# ANY WARRANTY expressed or implied, including the implied warranties of
9
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
10
# Public License for more details.  You should have received a copy of the
11
# GNU General Public License along with this program; if not, write to the
12
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
13
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
14
# source code or documentation are not subject to the GNU General Public
15
# License and may only be used or replicated with the express permission of
16
# Red Hat, Inc.
17
#
18
# Red Hat Author(s): Vratislav Podzimek <[email protected]>
19
#
20
21
import threading
22
import logging
23
from functools import wraps
24
25
# the path to addons is in sys.path so we can import things
26
# from org_fedora_oscap
27
from org_fedora_oscap import common
28
from org_fedora_oscap import data_fetch
29
from org_fedora_oscap import rule_handling
30
from org_fedora_oscap import content_handling
31
from org_fedora_oscap import utils
32
from org_fedora_oscap.constants import OSCAP
33
from org_fedora_oscap.structures import PolicyData
34
35
from pyanaconda.modules.common.constants.services import USERS
36
from pyanaconda.threading import threadMgr, AnacondaThread
37
from pyanaconda.ui.gui.spokes import NormalSpoke
38
from pyanaconda.ui.communication import hubQ
39
from pyanaconda.ui.gui.utils import async_action_wait, really_hide, really_show
40
from pyanaconda.ui.gui.utils import set_treeview_selection, fire_gtk_action
41
from pyanaconda.ui.categories.system import SystemCategory
42
from pykickstart.errors import KickstartValueError
43
44
# pylint: disable-msg=E0611
45
from gi.repository import Gdk
46
log = logging.getLogger("anaconda")
47
48
_ = common._
49
N_ = common.N_
50
51
# export only the spoke, no helper functions, classes or constants
52
__all__ = ["OSCAPSpoke"]
53
54
# pages in the main notebook
55
SET_PARAMS_PAGE = 0
56
GET_CONTENT_PAGE = 1
57
58
59
class GtkActionList(object):
60
    """Class for scheduling Gtk actions to be all run at once."""
61
62
    def __init__(self):
63
        self._actions = []
64
65
    def add_action(self, func, *args):
66
        """Add Gtk action to be run later."""
67
68
        @async_action_wait
69
        def gtk_action():
70
            func(*args)
71
72
        self._actions.append(gtk_action)
73
74
    def fire(self):
75
        """Run all scheduled Gtk actions."""
76
77
        for action in self._actions:
78
            action()
79
80
        self._actions = []
81
82
83
# helper functions
84
def set_combo_selection(combo, item, unset_first=False):
85
    """
86
    Set selected item of the combobox.
87
88
    :return: True if successfully set, False otherwise
89
    :rtype: bool
90
91
    """
92
93
    if unset_first:
94
        combo.set_active_iter(None)
95
96
    model = combo.get_model()
97
    if not model:
98
        return False
99
100
    itr = model.get_iter_first()
101
    while itr:
102
        if model[itr][0] == item:
103
            combo.set_active_iter(itr)
104
            return True
105
106
        itr = model.iter_next(itr)
107
108
        return False
109
110
111
def get_combo_selection(combo):
112
    """
113
    Get the selected item of the combobox.
114
115
    :return: selected item or None
116
117
    """
118
119
    model = combo.get_model()
120
    itr = combo.get_active_iter()
121
    if not itr or not model:
122
        return None
123
124
    return model[itr][0]
125
126
127
def render_message_type(column, renderer, model, itr, user_data=None):
128
    # get message type from the first column
129
    value = model[itr][0]
130
131
    if value == common.MESSAGE_TYPE_FATAL:
132
        renderer.set_property("stock-id", "gtk-dialog-error")
133
    elif value == common.MESSAGE_TYPE_WARNING:
134
        renderer.set_property("stock-id", "gtk-dialog-warning")
135
    elif value == common.MESSAGE_TYPE_INFO:
136
        renderer.set_property("stock-id", "gtk-info")
137
    else:
138
        renderer.set_property("stock-id", "gtk-dialog-question")
139
140
141
def set_ready(func):
142
    @wraps(func)
143
    def decorated(self, *args, **kwargs):
144
        ret = func(self, *args, **kwargs)
145
146
        self._unitialized_status = None
147
        self._ready = True
148
        # pylint: disable-msg=E1101
149
        hubQ.send_ready(self.__class__.__name__)
150
        hubQ.send_message(self.__class__.__name__, self.status)
151
152
        return ret
153
154
    return decorated
155
156
157
class OSCAPSpoke(NormalSpoke):
158
    """
159
    Main class of the OSCAP addon spoke that will appear in the Security
160
    category on the Summary hub. It allows interactive choosing of the data
161
    stream, checklist and profile driving the evaluation and remediation of the
162
    available SCAP content in the installation process.
163
164
    :see: pyanaconda.ui.common.UIObject
165
    :see: pyanaconda.ui.common.Spoke
166
    :see: pyanaconda.ui.gui.GUIObject
167
168
    """
169
170
    # class attributes defined by API #
171
172
    # list all top-level objects from the .glade file that should be exposed
173
    # to the spoke or leave empty to extract everything
174
    builderObjects = ["OSCAPspokeWindow", "profilesStore", "changesStore",
175
                      "dsStore", "xccdfStore", "profilesStore",
176
                      ]
177
178
    # the name of the main window widget
179
    mainWidgetName = "OSCAPspokeWindow"
180
181
    # name of the .glade file in the same directory as this source
182
    uiFile = "oscap.glade"
183
184
    # id of the help content for this spoke
185
    help_id = "SecurityPolicySpoke"
186
187
    # domain of oscap-anaconda-addon translations
188
    translationDomain = "oscap-anaconda-addon"
189
190
    # category this spoke belongs to
191
    category = SystemCategory
192
193
    # spoke icon (will be displayed on the hub)
194
    # preferred are the -symbolic icons as these are used in Anaconda's spokes
195
    icon = "changes-prevent-symbolic"
196
197
    # title of the spoke (will be displayed on the hub)
198
    title = N_("_Security Profile")
199
    # The string "SECURITY PROFILE" in oscap.glade is meant to be uppercase,
200
    # as it is displayed inside the spoke as the spoke label,
201
    # and spoke labels are all uppercase by a convention.
202
203
    # methods defined by API and helper methods #
204
    def __init__(self, data, storage, payload):
205
        """
206
        :see: pyanaconda.ui.common.Spoke.__init__
207
        :param data: data object passed to every spoke to load/store data
208
                     from/to it
209
        :type data: pykickstart.base.BaseHandler
210
        :param storage: object storing storage-related information
211
                        (disks, partitioning, bootloader, etc.)
212
        :type storage: blivet.Blivet
213
        :param payload: object storing packaging-related information
214
        :type payload: pyanaconda.packaging.Payload
215
216
        """
217
218
        NormalSpoke.__init__(self, data, storage, payload)
219
        # workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1673071
220
        self.title = _(self.title)
221
        self._storage = storage
222
        self._ready = False
223
224
        # the first status provided
225
        self._unitialized_status = _("Not ready")
226
227
        self._content_handler = None
228
        self._content_handling_cls = None
229
        self._ds_checklists = None
230
231
        # the proxy to OSCAP DBus module
232
        self._oscap_module = OSCAP.get_proxy()
233
234
        # the security policy data
235
        self._policy_enabled = self._oscap_module.PolicyEnabled
236
        self._policy_data = PolicyData.from_structure(
237
            self._oscap_module.PolicyData
238
        )
239
240
        # used for changing profiles
241
        self._rule_data = None
242
243
        # used for storing previously set root password if we need to remove it
244
        # due to the chosen policy (so that we can put it back in case of
245
        # revert)
246
        self.__old_root_pw = None
247
248
        # used to check if the profile was changed or not
249
        self._active_profile = None
250
251
        # prevent multiple simultaneous data fetches
252
        self._fetching = False
253
        self._fetch_flag_lock = threading.Lock()
254
255
        self._error = None
256
257
        # wait for all Anaconda spokes to initialiuze
258
        self._anaconda_spokes_initialized = threading.Event()
259
        self.initialization_controller.init_done.connect(self._all_anaconda_spokes_initialized)
260
261
    def _all_anaconda_spokes_initialized(self):
262
        log.debug("OSCAP addon: Anaconda init_done signal triggered")
263
        self._anaconda_spokes_initialized.set()
264
265
    @property
266
    def _content_defined(self):
267
        return self._policy_data.content_url \
268
            or self._policy_data.content_type == "scap-security-guide"
269
270
    def initialize(self):
271
        """
272
        The initialize method that is called after the instance is created.
273
        The difference between __init__ and this method is that this may take
274
        a long time and thus could be called in a separated thread.
275
276
        :see: pyanaconda.ui.common.UIObject.initialize
277
278
        """
279
280
        NormalSpoke.initialize(self)
281
        column = self.builder.get_object("messageTypeColumn")
282
        renderer = self.builder.get_object("messageTypeRenderer")
283
        column.set_cell_data_func(renderer, render_message_type)
284
285
        # the main notebook containing two pages -- for settings parameters and
286
        # for entering content URL
287
        self._main_notebook = self.builder.get_object("mainNotebook")
288
289
        # the store that holds the messages that come from the rules evaluation
290
        self._message_store = self.builder.get_object("changesStore")
291
292
        # stores with data streams, checklists and profiles
293
        self._ds_store = self.builder.get_object("dsStore")
294
        self._xccdf_store = self.builder.get_object("xccdfStore")
295
        self._profiles_store = self.builder.get_object("profilesStore")
296
297
        # comboboxes for data streams and checklists
298
        self._ids_box = self.builder.get_object("idsBox")
299
        self._ds_combo = self.builder.get_object("dsCombo")
300
        self._xccdf_combo = self.builder.get_object("xccdfCombo")
301
302
        # profiles view and selection
303
        self._profiles_view = self.builder.get_object("profilesView")
304
        self._profiles_selection = self.builder.get_object("profilesSelection")
305
        selected_column = self.builder.get_object("selectedColumn")
306
        selected_renderer = self.builder.get_object("selectedRenderer")
307
        selected_column.set_cell_data_func(selected_renderer,
308
                                           self._render_selected)
309
310
        # button for switching profiles
311
        self._choose_button = self.builder.get_object("chooseProfileButton")
312
313
        # toggle switching the dry-run mode
314
        self._dry_run_switch = self.builder.get_object("dryRunSwitch")
315
316
        # control buttons
317
        self._control_buttons = self.builder.get_object("controlButtons")
318
319
        # content URL entering, content fetching, ...
320
        self._no_content_label = self.builder.get_object("noContentLabel")
321
        self._content_url_entry = self.builder.get_object("urlEntry")
322
        self._fetch_button = self.builder.get_object("fetchButton")
323
        self._progress_box = self.builder.get_object("progressBox")
324
        self._progress_spinner = self.builder.get_object("progressSpinner")
325
        self._progress_label = self.builder.get_object("progressLabel")
326
        self._ssg_button = self.builder.get_object("ssgButton")
327
328
        # if no content was specified and SSG is available, use it
329
        if not self._policy_data.content_type and common.ssg_available():
330
            self._policy_data.content_type = "scap-security-guide"
331
            self._policy_data.content_path = common.SSG_DIR + common.SSG_CONTENT
332
333
        if not self._content_defined:
334
            # nothing more to be done now, the spoke is ready
335
            self._ready = True
336
337
            # no more being unitialized
338
            self._unitialized_status = None
339
340
            # user is going to enter the content URL
341
            self._content_url_entry.grab_focus()
342
343
            # pylint: disable-msg=E1101
344
            hubQ.send_ready(self.__class__.__name__)
345
        else:
346
            # else fetch data
347
            self._fetch_data_and_initialize()
348
349
    def _render_selected(self, column, renderer, model, itr, user_data=None):
350
        if model[itr][2]:
351
            renderer.set_property("stock-id", "gtk-apply")
352
        else:
353
            renderer.set_property("stock-id", None)
354
355
    def _fetch_data_and_initialize(self):
356
        """Fetch data from a specified URL and initialize everything."""
357
358
        with self._fetch_flag_lock:
359
            if self._fetching:
360
                # prevent multiple fetches running simultaneously
361
                return
362
            self._fetching = True
363
364
        thread_name = None
365
        if any(self._policy_data.content_url.startswith(net_prefix)
366
               for net_prefix in data_fetch.NET_URL_PREFIXES):
367
            # need to fetch data over network
368
            try:
369
                thread_name = common.wait_and_fetch_net_data(
370
                    self._policy_data.content_url,
371
                    common.get_raw_preinst_content_path(self._policy_data),
372
                    self._policy_data.certificates
373
                )
374
            except common.OSCAPaddonNetworkError:
375
                self._network_problem()
376
                with self._fetch_flag_lock:
377
                    self._fetching = False
378
                return
379
            except KickstartValueError:
380
                self._invalid_url()
381
                with self._fetch_flag_lock:
382
                    self._fetching = False
383
                return
384
385
        # pylint: disable-msg=E1101
386
        hubQ.send_message(self.__class__.__name__,
387
                          _("Fetching content data"))
388
        # pylint: disable-msg=E1101
389
        hubQ.send_not_ready(self.__class__.__name__)
390
        threadMgr.add(AnacondaThread(name="OSCAPguiWaitForDataFetchThread",
391
                                     target=self._init_after_data_fetch,
392
                                     args=(thread_name,)))
393
394
    @set_ready
395
    def _init_after_data_fetch(self, wait_for):
396
        """
397
        Waits for data fetching to be finished, extracts it (if needed),
398
        populates the stores and evaluates pre-installation fixes from the
399
        content and marks the spoke as ready in the end.
400
401
        :param wait_for: name of the thread to wait for (if any)
402
        :type wait_for: str or None
403
404
        """
405
406
        try:
407
            threadMgr.wait(wait_for)
408
        except data_fetch.DataFetchError:
409
            self._data_fetch_failed()
410
            with self._fetch_flag_lock:
411
                self._fetching = False
412
            return
413
        finally:
414
            # stop the spinner in any case
415
            fire_gtk_action(self._progress_spinner.stop)
416
417
        if self._policy_data.fingerprint:
418
            hash_obj = utils.get_hashing_algorithm(self._policy_data.fingerprint)
419
            digest = utils.get_file_fingerprint(self._policy_data.raw_preinst_content_path,
420
                                                hash_obj)
421
            if digest != self._policy_data.fingerprint:
422
                self._integrity_check_failed()
423
                # fetching done
424
                with self._fetch_flag_lock:
425
                    self._fetching = False
426
                return
427
428
        # RPM is an archive at this phase
429
        if self._policy_data.content_type in ("archive", "rpm"):
430
            # extract the content
431
            try:
432
                fpaths = common.extract_data(
433
                    common.get_raw_preinst_content_path(self._policy_data),
434
                    common.INSTALLATION_CONTENT_DIR,
435
                    [self._policy_data.content_path]
436
                )
437
            except common.ExtractionError as err:
438
                self._extraction_failed(str(err))
439
                # fetching done
440
                with self._fetch_flag_lock:
441
                    self._fetching = False
442
                return
443
444
            # and populate missing fields
445
            self._content_handling_cls, files = content_handling.explore_content_files(fpaths)
446
            files = common.strip_content_dir(files)
447
448
            # pylint: disable-msg=E1103
449
            self._policy_data.content_path = self._policy_data.content_path or files.xccdf
450
            self._policy_data.cpe_path = self._policy_data.cpe_path or files.cpe
451
            self._policy_data.tailoring_path = self._policy_data.tailoring_path or files.tailoring
452
        elif self._policy_data.content_type == "datastream":
453
            self._content_handling_cls = content_handling.DataStreamHandler
454
        elif self._policy_data.content_type == "scap-security-guide":
455
            self._content_handling_cls = content_handling.BenchmarkHandler
456
        else:
457
            raise common.OSCAPaddonError("Unsupported content type")
458
459
        try:
460
            self._content_handler = self._content_handling_cls(
461
                common.get_preinst_content_path(self._policy_data),
462
                common.get_preinst_tailoring_path(self._policy_data)
463
            )
464
        except content_handling.ContentHandlingError:
465
            self._invalid_content()
466
            # fetching done
467
            with self._fetch_flag_lock:
468
                self._fetching = False
469
470
            return
471
472
        if self._using_ds:
473
            # populate the stores from items from the content
474
            self._ds_checklists = self._content_handler.get_data_streams_checklists()
475
            add_ds_ids = GtkActionList()
476
            add_ds_ids.add_action(self._ds_store.clear)
477
            for dstream in self._ds_checklists.keys():
478
                add_ds_ids.add_action(self._add_ds_id, dstream)
479
            add_ds_ids.fire()
480
481
        self._update_ids_visibility()
482
483
        # refresh UI elements
484
        self._refresh_ui()
485
486
        # let all initialization and configuration happen before we evaluate
487
        # the setup
488
        if not self._anaconda_spokes_initialized.is_set():
489
            # only wait (and log the messages) if the event is not set yet
490
            log.debug("OSCAP addon: waiting for all Anaconda spokes to be initialized")
491
            self._anaconda_spokes_initialized.wait()
492
            log.debug("OSCAP addon: all Anaconda spokes have been initialized - continuing")
493
494
        # try to switch to the chosen profile (if any)
495
        selected = self._switch_profile()
496
497
        if self._policy_data.profile_id and not selected:
498
            # profile ID given, but it was impossible to select it -> invalid
499
            # profile ID given
500
            self._invalid_profile_id()
501
            return
502
503
        # update the message store with the messages
504
        self._update_message_store()
505
506
        # all initialized, we can now let user set parameters
507
        fire_gtk_action(self._main_notebook.set_current_page, SET_PARAMS_PAGE)
508
509
        # and use control buttons
510
        fire_gtk_action(really_show, self._control_buttons)
511
512
        # fetching done
513
        with self._fetch_flag_lock:
514
            self._fetching = False
515
516
        # no error
517
        self._set_error(None)
518
519
    @property
520
    def _using_ds(self):
521
        return self._content_handling_cls == content_handling.DataStreamHandler
522
523
    @property
524
    def _current_ds_id(self):
525
        return get_combo_selection(self._ds_combo)
526
527
    @property
528
    def _current_xccdf_id(self):
529
        return get_combo_selection(self._xccdf_combo)
530
531
    @property
532
    def _current_profile_id(self):
533
        store, itr = self._profiles_selection.get_selected()
534
        if not store or not itr:
535
            return None
536
        else:
537
            return store[itr][0]
538
539
    def _add_ds_id(self, ds_id):
540
        """
541
        Add data stream ID to the data streams store.
542
543
        :param ds_id: data stream ID
544
        :type ds_id: str
545
546
        """
547
548
        self._ds_store.append([ds_id])
549
550
    @async_action_wait
551
    def _update_ids_visibility(self):
552
        """
553
        Updates visibility of the combo boxes that are used to select the DS
554
        and XCCDF IDs.
555
556
        """
557
558
        if self._using_ds:
559
            # only show the combo boxes if there are multiple data streams or
560
            # multiple xccdfs (IOW if there's something to choose from)
561
            ds_ids = list(self._ds_checklists.keys())
562
            if len(ds_ids) > 1 or len(self._ds_checklists[ds_ids[0]]) > 1:
563
                really_show(self._ids_box)
564
                return
565
566
        # not showing, hide instead
567
        really_hide(self._ids_box)
568
569
    @async_action_wait
570
    def _update_xccdfs_store(self):
571
        """
572
        Clears and repopulates the store with XCCDF IDs from the currently
573
        selected data stream.
574
575
        """
576
577
        if self._ds_checklists is None:
578
            # not initialized, cannot do anything
579
            return
580
581
        self._xccdf_store.clear()
582
        for xccdf_id in self._ds_checklists[self._current_ds_id]:
583
            self._xccdf_store.append([xccdf_id])
584
585
    @async_action_wait
586
    def _update_profiles_store(self):
587
        """
588
        Clears and repopulates the store with profiles from the currently
589
        selected data stream and checklist.
590
591
        """
592
593
        if self._content_handler is None:
594
            # not initialized, cannot do anything
595
            return
596
597
        if self._using_ds and self._ds_checklists is None:
598
            # not initialized, cannot do anything
599
            return
600
601
        self._profiles_store.clear()
602
603
        if self._using_ds:
604
            profiles = self._content_handler.get_profiles(self._current_ds_id,
605
                                                          self._current_xccdf_id)
606
        else:
607
            # pylint: disable-msg=E1103
608
            profiles = self._content_handler.profiles
609
610
        for profile in profiles:
611
            profile_markup = '<span weight="bold">%s</span>\n%s' \
612
                                % (profile.title, profile.description)
613
            self._profiles_store.append([profile.id,
614
                                         profile_markup,
615
                                         profile.id == self._active_profile])
616
617
    def _add_message(self, message):
618
        """
619
        Add message to the store.
620
621
        :param message: message to be added
622
        :type message: org_fedora_oscap.common.RuleMessage
623
624
        """
625
626
        self._message_store.append([message.type, message.text])
627
628
    @async_action_wait
629
    def _update_message_store(self, report_only=False):
630
        """
631
        Updates the message store with messages from rule evaluation.
632
633
        :param report_only: wheter to do changes in configuration or just
634
                            report
635
        :type report_only: bool
636
637
        """
638
        if not self._policy_enabled:
639
            return
640
641
        self._message_store.clear()
642
643
        if not self._rule_data:
644
            # RuleData instance not initialized, cannot do anything
645
            return
646
647
        messages = self._rule_data.eval_rules(self.data, self._storage,
648
                                              report_only)
649
        if not messages:
650
            # no messages from the rules, add a message informing about that
651
            if not self._active_profile:
652
                # because of no profile
653
                message = common.RuleMessage(self.__class__,
654
                                             common.MESSAGE_TYPE_INFO,
655
                                             _("No profile selected"))
656
            else:
657
                # because of no pre-inst rules
658
                message = common.RuleMessage(self.__class__,
659
                                             common.MESSAGE_TYPE_INFO,
660
                                             _("No rules for the pre-installation phase"))
661
            self._add_message(message)
662
663
            # nothing more to be done
664
            return
665
666
        self._resolve_rootpw_issues(messages, report_only)
667
        for msg in messages:
668
            self._add_message(msg)
669
670
    def _resolve_rootpw_issues(self, messages, report_only):
671
        """Mitigate root password issues (which are not fatal in GUI)"""
672
        fatal_rootpw_msgs = [
673
            msg for msg in messages
674
            if msg.origin == rule_handling.PasswdRules and msg.type == common.MESSAGE_TYPE_FATAL]
675
676
        if fatal_rootpw_msgs:
677
            for msg in fatal_rootpw_msgs:
678
                # cannot just change the message type because it is a namedtuple
679
                messages.remove(msg)
680
681
                msg = common.RuleMessage(
682
                    self.__class__, common.MESSAGE_TYPE_WARNING, msg.text)
683
                messages.append(msg)
684
685
            if not report_only:
686
                users_proxy = USERS.get_proxy()
687
688
                self.__old_root_pw = users_proxy.RootPassword
689
                self.data.rootpw.password = None
690
                self.__old_root_pw_seen = users_proxy.IsRootpwKickstarted
691
                self.data.rootpw.seen = False
692
693
    def _revert_rootpw_changes(self):
694
        if self.__old_root_pw is not None:
695
            users_proxy = USERS.get_proxy()
696
697
            users_proxy.SetRootPassword(self.__old_root_pw)
698
            self.__old_root_pw = None
699
700
            users_proxy.SetRootpwKickstarted(self.__old_root_pw_seen)
701
            self.__old_root_pw_seen = None
702
703
    @async_action_wait
704
    def _unselect_profile(self, profile_id):
705
        """Unselects the given profile."""
706
707
        if not profile_id:
708
            # no profile specified, nothing to do
709
            return
710
711
        itr = self._profiles_store.get_iter_first()
712
        while itr:
713
            if self._profiles_store[itr][0] == profile_id:
714
                self._profiles_store.set_value(itr, 2, False)
715
            itr = self._profiles_store.iter_next(itr)
716
717
        if self._rule_data:
718
            # revert changes and clear rule_data (no longer valid)
719
            self._rule_data.revert_changes(self.data, self._storage)
720
            self._revert_rootpw_changes()
721
            self._rule_data = None
722
723
        self._active_profile = None
724
725
    @async_action_wait
726
    def _select_profile(self, profile_id):
727
        """Selects the given profile."""
728
729
        if not profile_id:
730
            # no profile specified, nothing to do
731
            return False
732
733
        if self._using_ds:
734
            ds = self._current_ds_id
735
            xccdf = self._current_xccdf_id
736
737
            if not all((ds, xccdf, profile_id)):
738
                # something is not set -> do nothing
739
                return False
740
        else:
741
            ds = None
742
            xccdf = None
743
744
        # get pre-install fix rules from the content
745
        try:
746
            rules = common.get_fix_rules_pre(
747
                profile_id,
748
                common.get_preinst_content_path(self._policy_data),
749
                ds, xccdf,
750
                common.get_preinst_tailoring_path(self._policy_data)
751
            )
752
        except common.OSCAPaddonError as exc:
753
            log.error(
754
                "Failed to get rules for the profile '{}': {}"
755
                .format(profile_id, str(exc)))
756
            self._set_error(
757
                "Failed to get rules for the profile '{}'"
758
                .format(profile_id))
759
            return False
760
761
        itr = self._profiles_store.get_iter_first()
762
        while itr:
763
            if self._profiles_store[itr][0] == profile_id:
764
                self._profiles_store.set_value(itr, 2, True)
765
            itr = self._profiles_store.iter_next(itr)
766
767
        # parse and store rules with a clean RuleData instance
768
        self._rule_data = rule_handling.RuleData()
769
        for rule in rules.splitlines():
770
            self._rule_data.new_rule(rule)
771
772
        # remember the active profile
773
        self._active_profile = profile_id
774
775
        return True
776
777
    @async_action_wait
778
    def _switch_profile(self):
779
        """Switches to a current selected profile.
780
781
        :returns: whether some profile was selected or not
782
783
        """
784
        if not self._policy_enabled:
785
            return False
786
787
        self._set_error(None)
788
        profile = self._current_profile_id
789
        if not profile:
790
            return False
791
792
        self._unselect_profile(self._active_profile)
793
        ret = self._select_profile(profile)
794
795
        # update messages according to the newly chosen profile
796
        self._update_message_store()
797
798
        return ret
799
800
    @set_ready
801
    def _set_error(self, msg):
802
        """Set or clear error message"""
803
        if msg:
804
            self._error = msg
805
            self.clear_info()
806
            self.set_error(msg)
807
        else:
808
            self._error = None
809
            self.clear_info()
810
811
    @async_action_wait
812
    def _invalid_content(self):
813
        """Callback for informing user about provided content invalidity."""
814
815
        msg = _("Invalid content provided. Enter a different URL, please.")
816
        self._progress_label.set_markup("<b>%s</b>" % msg)
817
        self._wrong_content(msg)
818
819
    @async_action_wait
820
    def _invalid_url(self):
821
        """Callback for informing user about provided URL invalidity."""
822
823
        msg = _("Invalid or unsupported content URL, please enter a different one.")
824
        self._progress_label.set_markup("<b>%s</b>" % msg)
825
        self._wrong_content(msg)
826
827
    @async_action_wait
828
    def _data_fetch_failed(self):
829
        """Adapts the UI if fetching data from entered URL failed"""
830
831
        msg = _("Failed to fetch content. Enter a different URL, please.")
832
        self._progress_label.set_markup("<b>%s</b>" % msg)
833
        self._wrong_content(msg)
834
835
    @async_action_wait
836
    def _network_problem(self):
837
        """Adapts the UI if network error was encountered during data fetch"""
838
839
        msg = _("Network error encountered when fetching data."
840
                " Please check that network is setup and working.")
841
        self._progress_label.set_markup("<b>%s</b>" % msg)
842
        self._wrong_content(msg)
843
844
    @async_action_wait
845
    def _integrity_check_failed(self):
846
        """Adapts the UI if integrity check fails"""
847
848
        msg = _("The integrity check of the content failed. Cannot use the content.")
849
        self._progress_label.set_markup("<b>%s</b>" % msg)
850
        self._wrong_content(msg)
851
852
    @async_action_wait
853
    def _extraction_failed(self, err_msg):
854
        """Adapts the UI if extracting data from entered URL failed"""
855
856
        msg = _("Failed to extract content (%s). Enter a different URL, "
857
                "please.") % err_msg
858
        self._progress_label.set_markup("<b>%s</b>" % msg)
859
        self._wrong_content(msg)
860
861
    @async_action_wait
862
    def _wrong_content(self, msg):
863
        self._policy_data = PolicyData()
864
        really_hide(self._progress_spinner)
865
        self._fetch_button.set_sensitive(True)
866
        self._content_url_entry.set_sensitive(True)
867
        self._content_url_entry.grab_focus()
868
        self._content_url_entry.select_region(0, -1)
869
        self._content_handling_cls = None
870
        self._set_error(msg)
871
872
    @async_action_wait
873
    def _invalid_profile_id(self):
874
        msg = _(
875
            "Profile with ID '%s' not defined in the content. "
876
            "Select a different profile, please"
877
        ) % self._policy_data.profile_id
878
879
        self._set_error(msg)
880
        self._policy_data.profile_id = ""
881
882
    @async_action_wait
883
    def _switch_dry_run(self, dry_run):
884
        self._choose_button.set_sensitive(not dry_run)
885
886
        if dry_run:
887
            # no profile can be selected in the dry-run mode
888
            self._unselect_profile(self._active_profile)
889
890
            # no messages in the dry-run mode
891
            self._message_store.clear()
892
            message = common.RuleMessage(self.__class__,
893
                                         common.MESSAGE_TYPE_INFO,
894
                                         _("Not applying security profile"))
895
            self._add_message(message)
896
897
            self._set_error(None)
898
        else:
899
            # mark the active profile as selected
900
            self._select_profile(self._active_profile)
901
            self._update_message_store()
902
903
    @async_action_wait
904
    def refresh(self):
905
        """
906
        The refresh method that is called every time the spoke is displayed.
907
        It should update the UI elements according to the contents of
908
        self.data.
909
910
        :see: pyanaconda.ui.common.UIObject.refresh
911
912
        """
913
        # update the security policy data
914
        self._policy_enabled = self._oscap_module.PolicyEnabled
915
        self._policy_data = PolicyData.from_structure(
916
            self._oscap_module.PolicyData
917
        )
918
919
        # update the UI elements
920
        self._refresh_ui()
921
922
    def _refresh_ui(self):
923
        """Refresh the UI elements."""
924
        if not self._content_defined:
925
            # hide the control buttons
926
            really_hide(self._control_buttons)
927
928
            # provide SSG if available
929
            if common.ssg_available():
930
                # show the SSG button and tweak the rest of the line
931
                # (the label)
932
                really_show(self._ssg_button)
933
                # TRANSLATORS: the other choice if SCAP Security Guide is also
934
                # available
935
                tip = _(" or enter data stream content or archive URL below:")
936
            else:
937
                # hide the SSG button
938
                really_hide(self._ssg_button)
939
                tip = _("No content found. Please enter data stream content or "
940
                        "archive URL below:")
941
942
            self._no_content_label.set_text(tip)
943
944
            # hide the progress box, no progress now
945
            with self._fetch_flag_lock:
946
                if not self._fetching:
947
                    really_hide(self._progress_box)
948
949
                    self._content_url_entry.set_sensitive(True)
950
                    self._fetch_button.set_sensitive(True)
951
952
                    if not self._content_url_entry.get_text():
953
                        # no text -> no info/warning
954
                        self._progress_label.set_text("")
955
956
            # switch to the page allowing user to enter content URL and fetch
957
            # it
958
            self._main_notebook.set_current_page(GET_CONTENT_PAGE)
959
            self._content_url_entry.grab_focus()
960
961
            # nothing more to do here
962
            return
963
        else:
964
            # show control buttons
965
            really_show(self._control_buttons)
966
967
            self._main_notebook.set_current_page(SET_PARAMS_PAGE)
968
969
        self._active_profile = self._policy_data.profile_id
970
971
        self._update_ids_visibility()
972
973
        if self._using_ds:
974
            if self._policy_data.datastream_id:
975
                set_combo_selection(self._ds_combo,
976
                                    self._policy_data.datastream_id,
977
                                    unset_first=True)
978
            else:
979
                try:
980
                    default_ds = next(iter(self._ds_checklists.keys()))
981
                    set_combo_selection(self._ds_combo, default_ds,
982
                                        unset_first=True)
983
                except StopIteration:
984
                    # no data stream available
985
                    pass
986
987
                if self._policy_data.datastream_id and self._policy_data.xccdf_id:
988
                    set_combo_selection(self._xccdf_combo,
989
                                        self._policy_data.xccdf_id,
990
                                        unset_first=True)
991
        else:
992
            # no combobox changes --> need to update profiles store manually
993
            self._update_profiles_store()
994
995
        if self._policy_data.profile_id:
996
            set_treeview_selection(self._profiles_view,
997
                                   self._policy_data.profile_id)
998
999
        self._update_message_store()
1000
1001
    def apply(self):
1002
        """
1003
        The apply method that is called when the spoke is left. It should
1004
        update the contents of self.data with values set in the GUI elements.
1005
1006
        """
1007
1008
        if not self._content_defined or not self._active_profile:
1009
            # no errors for no content or no profile
1010
            self._set_error(None)
1011
1012
        # store currently selected values to the addon data attributes
1013
        if self._using_ds:
1014
            self._policy_data.datastream_id = self._current_ds_id
1015
            self._policy_data.xccdf_id = self._current_xccdf_id
1016
1017
        self._policy_data.profile_id = self._active_profile
1018
        self._policy_enabled = self._dry_run_switch.get_active()
1019
1020
        # set up the DBus module
1021
        self._oscap_module.PolicyData = PolicyData.to_structure(self._policy_data)
1022
        self._oscap_module.PolicyEnabled = self._policy_enabled
1023
1024
    def execute(self):
1025
        """
1026
        The excecute method that is called when the spoke is left. It is
1027
        supposed to do all changes to the runtime environment according to
1028
        the values set in the GUI elements.
1029
1030
        """
1031
1032
        # nothing to do here
1033
        pass
1034
1035
    @property
1036
    def ready(self):
1037
        """
1038
        The ready property that tells whether the spoke is ready (can be
1039
        visited) or not.
1040
1041
        :rtype: bool
1042
1043
        """
1044
1045
        return self._ready
1046
1047
    @property
1048
    def completed(self):
1049
        """
1050
        The completed property that tells whether all mandatory items on the
1051
        spoke are set, or not. The spoke will be marked on the hub as completed
1052
        or uncompleted acording to the returned value.
1053
1054
        :rtype: bool
1055
1056
        """
1057
1058
        # no error message in the store
1059
        return not self._error and all(row[0] != common.MESSAGE_TYPE_FATAL
1060
                                       for row in self._message_store)
1061
1062
    @property
1063
    @async_action_wait
1064
    def status(self):
1065
        """
1066
        The status property that is a brief string describing the state of the
1067
        spoke. It should describe whether all values are set and if possible
1068
        also the values themselves. The returned value will appear on the hub
1069
        below the spoke's title.
1070
1071
        :rtype: str
1072
1073
        """
1074
1075
        if self._error:
1076
            return _("Error fetching and loading content")
1077
1078
        if self._unitialized_status:
1079
            # not initialized
1080
            return self._unitialized_status
1081
1082
        if not self._content_defined:
1083
            return _("No content found")
1084
1085
        if not self._active_profile:
1086
            return _("No profile selected")
1087
1088
        # update message store, something may changed from the last update
1089
        self._update_message_store(report_only=True)
1090
1091
        warning_found = False
1092
        for row in self._message_store:
1093
            if row[0] == common.MESSAGE_TYPE_FATAL:
1094
                return _("Misconfiguration detected")
1095
            elif row[0] == common.MESSAGE_TYPE_WARNING:
1096
                warning_found = True
1097
1098
        # TODO: at least the last two status messages need a better wording
1099
        if warning_found:
1100
            return _("Warnings appeared")
1101
1102
        return _("Everything okay")
1103
1104
    def on_ds_combo_changed(self, *args):
1105
        """Handler for the datastream ID change."""
1106
1107
        ds_id = self._current_ds_id
1108
        if not ds_id:
1109
            return
1110
1111
        self._update_xccdfs_store()
1112
        first_checklist = self._ds_checklists[ds_id][0]
1113
1114
        set_combo_selection(self._xccdf_combo, first_checklist)
1115
1116
    def on_xccdf_combo_changed(self, *args):
1117
        """Handler for the XCCDF ID change."""
1118
1119
        # may take a while
1120
        self._update_profiles_store()
1121
1122
    def on_profiles_selection_changed(self, *args):
1123
        """Handler for the profile selection change."""
1124
        if not self._policy_enabled:
1125
            return
1126
1127
        cur_profile = self._current_profile_id
1128
        if cur_profile:
1129
            if cur_profile != self._active_profile:
1130
                # new profile selected, make the selection button sensitive
1131
                self._choose_button.set_sensitive(True)
1132
            else:
1133
                # current active profile selected
1134
                self._choose_button.set_sensitive(False)
1135
1136
    def on_profile_clicked(self, widget, event, *args):
1137
        """Handler for the profile being clicked on."""
1138
        if not self._policy_enabled:
1139
            return
1140
1141
        # if a profile is double-clicked, we should switch to it
1142
        if event.type == Gdk.EventType._2BUTTON_PRESS:
1143
            self._switch_profile()
1144
1145
            # active profile selected
1146
            self._choose_button.set_sensitive(False)
1147
1148
        # let the other actions hooked to the click happen as well
1149
        return False
1150
1151
    def on_profile_chosen(self, *args):
1152
        """
1153
        Handler for the profile being chosen
1154
        (e.g. "Select profile" button hit).
1155
1156
        """
1157
1158
        # switch profile
1159
        self._switch_profile()
1160
1161
        # active profile selected
1162
        self._choose_button.set_sensitive(False)
1163
1164
    def on_fetch_button_clicked(self, *args):
1165
        """Handler for the Fetch button"""
1166
1167
        with self._fetch_flag_lock:
1168
            if self._fetching:
1169
                # some other fetching/pre-processing running, give up
1170
                return
1171
1172
        # prevent user from changing the URL in the meantime
1173
        self._content_url_entry.set_sensitive(False)
1174
        self._fetch_button.set_sensitive(False)
1175
        url = self._content_url_entry.get_text()
1176
        really_show(self._progress_box)
1177
        really_show(self._progress_spinner)
1178
1179
        if not data_fetch.can_fetch_from(url):
1180
            msg = _("Invalid or unsupported URL")
1181
            # cannot start fetching
1182
            self._progress_label.set_markup("<b>%s</b>" % msg)
1183
            self._wrong_content(msg)
1184
            return
1185
1186
        self._progress_label.set_text(_("Fetching content..."))
1187
        self._progress_spinner.start()
1188
        self._policy_data.content_url = url
1189
        if url.endswith(".rpm"):
1190
            self._policy_data.content_type = "rpm"
1191
        elif any(url.endswith(arch_type) for arch_type in common.SUPPORTED_ARCHIVES):
1192
            self._policy_data.content_type = "archive"
1193
        else:
1194
            self._policy_data.content_type = "datastream"
1195
1196
        self._fetch_data_and_initialize()
1197
1198
    def on_dry_run_toggled(self, switch, *args):
1199
        dry_run = not switch.get_active()
1200
        self._policy_enabled = not dry_run
1201
        self._switch_dry_run(dry_run)
1202
1203
    def on_change_content_clicked(self, *args):
1204
        self._unselect_profile(self._active_profile)
1205
        self._policy_data = PolicyData()
1206
        self._refresh_ui()
1207
1208
    def on_use_ssg_clicked(self, *args):
1209
        self._policy_data = PolicyData()
1210
        self._policy_data.content_type = "scap-security-guide"
1211
        self._policy_data.content_path = common.SSG_DIR + common.SSG_CONTENT
1212
        self._fetch_data_and_initialize()
1213