Passed
Pull Request — rhel9-branch (#223)
by Matěj
01:51
created

set_combo_selection()   A

Complexity

Conditions 5

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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