Passed
Pull Request — rhel8-branch (#178)
by Matěj
01:59
created

OSCAPSpoke.completed()   A

Complexity

Conditions 1

Size

Total Lines 14
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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