Passed
Push — rhel9-branch ( 6fbab8...4bb28d )
by Matěj
01:36 queued 12s
created

OSCAPSpoke._still_fetching()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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