OSCAPSpoke._invalid_profile_id()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nop 1
dl 0
loc 9
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.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
        hubQ.send_message(self.__class__.__name__, self.status)
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
245
        # used for changing profiles
246
        self._rule_data = None
247
248
        # used for storing previously set root password if we need to remove it
249
        # due to the chosen policy (so that we can put it back in case of
250
        # revert)
251
        self.__old_root_pw = None
252
253
        # used to check if the profile was changed or not
254
        self._active_profile = ""
255
256
        # prevent multiple simultaneous data fetches
257
        self._fetching = False
258
        self._fetch_flag_lock = threading.Lock()
259
260
        self._error = None
261
262
        # wait for all Anaconda spokes to initialiuze
263
        self._anaconda_spokes_initialized = threading.Event()
264
        self.initialization_controller.init_done.connect(self._all_anaconda_spokes_initialized)
265
266
        self.content_bringer = content_discovery.ContentBringer(self._policy_data)
267
        self._content_handler = None
268
269
    def _all_anaconda_spokes_initialized(self):
270
        log.debug("OSCAP addon: Anaconda init_done signal triggered")
271
        self._anaconda_spokes_initialized.set()
272
273
    @property
274
    def _content_defined(self):
275
        return self._policy_data.content_url \
276
            or self._policy_data.content_type == "scap-security-guide"
277
278
    def initialize(self):
279
        """
280
        The initialize method that is called after the instance is created.
281
        The difference between __init__ and this method is that this may take
282
        a long time and thus could be called in a separated thread.
283
284
        :see: pyanaconda.ui.common.UIObject.initialize
285
286
        """
287
288
        NormalSpoke.initialize(self)
289
        column = self.builder.get_object("messageTypeColumn")
290
        renderer = self.builder.get_object("messageTypeRenderer")
291
        column.set_cell_data_func(renderer, render_message_type)
292
293
        # the main notebook containing two pages -- for settings parameters and
294
        # for entering content URL
295
        self._main_notebook = self.builder.get_object("mainNotebook")
296
297
        # the store that holds the messages that come from the rules evaluation
298
        self._message_store = self.builder.get_object("changesStore")
299
300
        # stores with data streams, checklists and profiles
301
        self._ds_store = self.builder.get_object("dsStore")
302
        self._xccdf_store = self.builder.get_object("xccdfStore")
303
        self._profiles_store = self.builder.get_object("profilesStore")
304
305
        # comboboxes for data streams and checklists
306
        self._ids_box = self.builder.get_object("idsBox")
307
        self._ds_combo = self.builder.get_object("dsCombo")
308
        self._xccdf_combo = self.builder.get_object("xccdfCombo")
309
310
        # profiles view and selection
311
        self._profiles_view = self.builder.get_object("profilesView")
312
        self._profiles_selection = self.builder.get_object("profilesSelection")
313
        selected_column = self.builder.get_object("selectedColumn")
314
        selected_renderer = self.builder.get_object("selectedRenderer")
315
        selected_column.set_cell_data_func(selected_renderer,
316
                                           self._render_selected)
317
318
        # button for switching profiles
319
        self._choose_button = self.builder.get_object("chooseProfileButton")
320
321
        # toggle switching the dry-run mode
322
        self._dry_run_switch = self.builder.get_object("dryRunSwitch")
323
324
        # control buttons
325
        self._control_buttons = self.builder.get_object("controlButtons")
326
327
        # content URL entering, content fetching, ...
328
        self._no_content_label = self.builder.get_object("noContentLabel")
329
        self._content_url_entry = self.builder.get_object("urlEntry")
330
        self._fetch_button = self.builder.get_object("fetchButton")
331
        self._progress_box = self.builder.get_object("progressBox")
332
        self._progress_spinner = self.builder.get_object("progressSpinner")
333
        self._progress_label = self.builder.get_object("progressLabel")
334
        self._ssg_button = self.builder.get_object("ssgButton")
335
336
        # if no content was specified and SSG is available, use it
337
        if not self._policy_data.content_type and common.ssg_available():
338
            log.info("OSCAP Addon: Defaulting to local content")
339
            self._policy_data.content_type = "scap-security-guide"
340
            self._policy_data.content_path = common.SSG_DIR + common.SSG_CONTENT
341
            self._save_policy_data()
342
343
        if not self._content_defined:
344
            # nothing more to be done now, the spoke is ready
345
            self._ready = True
346
347
            # no more being unitialized
348
            self._unitialized_status = None
349
350
            # user is going to enter the content URL
351
            self._content_url_entry.grab_focus()
352
353
            # pylint: disable-msg=E1101
354
            hubQ.send_ready(self.__class__.__name__)
355
        else:
356
            # else fetch data
357
            self._fetch_data_and_initialize()
358
359
    def _save_policy_data(self):
360
        self._oscap_module.PolicyData = PolicyData.to_structure(self._policy_data)
361
        self._oscap_module.PolicyEnabled = self._policy_enabled
362
363
    def _load_policy_data(self):
364
        self._policy_data.update_from(PolicyData.from_structure(
365
            self._oscap_module.PolicyData
366
        ))
367
        self._policy_enabled = self._oscap_module.PolicyEnabled
368
369
    def _handle_error(self, exception):
370
        log.error("OSCAP Addon: " + str(exception))
371
        if isinstance(exception, KickstartValueError):
372
            self._invalid_url()
373
        elif isinstance(exception, common.OSCAPaddonNetworkError):
374
            self._network_problem()
375
        elif isinstance(exception, data_fetch.DataFetchError):
376
            self._data_fetch_failed()
377
        elif isinstance(exception, common.ExtractionError):
378
            self._extraction_failed(str(exception))
379
        elif isinstance(exception, content_handling.ContentHandlingError):
380
            self._invalid_content()
381
        elif isinstance(exception, content_handling.ContentCheckError):
382
            self._integrity_check_failed()
383
        else:
384
            log.exception("OSCAP Addon: Unknown exception occurred", exc_info=exception)
385
            self._general_content_problem()
386
387
    def _render_selected(self, column, renderer, model, itr, user_data=None):
388
        if model[itr][2]:
389
            renderer.set_property("stock-id", "gtk-apply")
390
        else:
391
            renderer.set_property("stock-id", None)
392
393
    def _fetch_data_and_initialize(self):
394
        """Fetch data from a specified URL and initialize everything."""
395
396
        with self._fetch_flag_lock:
397
            if self._fetching:
398
                # prevent multiple fetches running simultaneously
399
                return
400
            self._fetching = True
401
402
        thread_name = None
403
        if self._policy_data.content_url and self._policy_data.content_type != "scap-security-guide":
404
            log.info(f"OSCAP Addon: Actually fetching content from somewhere")
405
            thread_name = self.content_bringer.fetch_content(
406
                self._handle_error, 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
        def update_progress_label(msg):
429
            fire_gtk_action(self._progress_label.set_text(msg))
430
431
        content_path = None
432
        actually_fetched_content = wait_for is not None
433
434
        if actually_fetched_content:
435
            content_path = common.get_raw_preinst_content_path(self._policy_data)
436
437
        content = self.content_bringer.finish_content_fetch(
438
            wait_for, self._policy_data.fingerprint, update_progress_label,
439
            content_path, self._handle_error)
440
        if not content:
441
            with self._fetch_flag_lock:
442
                self._fetching = False
443
            return
444
445
        fire_gtk_action(self._progress_spinner.stop)
446
        fire_gtk_action(
447
            self._progress_label.set_text,
448
            _("Fetch complete, analyzing data."))
449
450
        try:
451
            if actually_fetched_content:
452
                self.content_bringer.use_downloaded_content(content)
453
454
            preinst_content_path = common.get_preinst_content_path(self._policy_data)
455
            preinst_tailoring_path = common.get_preinst_tailoring_path(self._policy_data)
456
457
            msg = f"Opening SCAP content at {preinst_content_path}"
458
            if self._policy_data.tailoring_path:
459
                msg += f" with tailoring {preinst_tailoring_path}"
460
            else:
461
                msg += " without considering tailoring"
462
            log.info("OSCAP Addon: " + msg)
463
464
            self._content_handler = scap_content_handler.SCAPContentHandler(
465
                preinst_content_path,
466
                preinst_tailoring_path)
467
        except Exception as e:
468
            log.error(str(e))
469
            self._invalid_content()
470
            # fetching done
471
            with self._fetch_flag_lock:
472
                self._fetching = False
473
474
            return
475
476
        log.info("OSCAP Addon: Done with analysis")
477
478
        self._ds_checklists = self._content_handler.get_data_streams_checklists()
479
        if self._using_ds:
480
            # populate the stores from items from the content
481
            add_ds_ids = GtkActionList()
482
            add_ds_ids.add_action(self._ds_store.clear)
483
            for dstream in self._ds_checklists.keys():
484
                add_ds_ids.add_action(self._add_ds_id, dstream)
485
            add_ds_ids.fire()
486
487
        self._update_ids_visibility()
488
489
        # refresh UI elements
490
        self._refresh_ui()
491
492
        # let all initialization and configuration happen before we evaluate
493
        # the setup
494
        if not self._anaconda_spokes_initialized.is_set():
495
            # only wait (and log the messages) if the event is not set yet
496
            log.debug("OSCAP addon: waiting for all Anaconda spokes to be initialized")
497
            self._anaconda_spokes_initialized.wait()
498
            log.debug("OSCAP addon: all Anaconda spokes have been initialized - continuing")
499
500
        # try to switch to the chosen profile (if any)
501
        selected = self._switch_profile()
502
503
        if self._policy_data.profile_id and not selected:
504
            # profile ID given, but it was impossible to select it -> invalid
505
            # profile ID given
506
            self._invalid_profile_id()
507
            return
508
509
        # update the message store with the messages
510
        self._update_message_store()
511
512
        # all initialized, we can now let user set parameters
513
        fire_gtk_action(self._main_notebook.set_current_page, SET_PARAMS_PAGE)
514
515
        # and use control buttons
516
        fire_gtk_action(really_show, self._control_buttons)
517
518
        # fetching done
519
        with self._fetch_flag_lock:
520
            self._fetching = False
521
522
        # no error
523
        self._set_error(None)
524
525
    @property
526
    def _using_ds(self):
527
        return self._ds_checklists is not None
528
529
    @property
530
    def _current_ds_id(self):
531
        return get_combo_selection(self._ds_combo)
532
533
    @property
534
    def _current_xccdf_id(self):
535
        return get_combo_selection(self._xccdf_combo)
536
537
    @property
538
    def _current_profile_id(self):
539
        store, itr = self._profiles_selection.get_selected()
540
        if not store or not itr:
541
            return None
542
        else:
543
            return store[itr][0]
544
545
    def _add_ds_id(self, ds_id):
546
        """
547
        Add data stream ID to the data streams store.
548
549
        :param ds_id: data stream ID
550
        :type ds_id: str
551
552
        """
553
554
        self._ds_store.append([ds_id])
555
556
    @async_action_wait
557
    def _update_ids_visibility(self):
558
        """
559
        Updates visibility of the combo boxes that are used to select the DS
560
        and XCCDF IDs.
561
562
        """
563
564
        if self._using_ds:
565
            # only show the combo boxes if there are multiple data streams or
566
            # multiple xccdfs (IOW if there's something to choose from)
567
            ds_ids = list(self._ds_checklists.keys())
568
            if len(ds_ids) > 1 or len(self._ds_checklists[ds_ids[0]]) > 1:
569
                really_show(self._ids_box)
570
                return
571
572
        # not showing, hide instead
573
        really_hide(self._ids_box)
574
575
    @async_action_wait
576
    def _update_xccdfs_store(self):
577
        """
578
        Clears and repopulates the store with XCCDF IDs from the currently
579
        selected data stream.
580
581
        """
582
583
        if self._ds_checklists is None:
584
            # not initialized, cannot do anything
585
            return
586
587
        self._xccdf_store.clear()
588
        for xccdf_id in self._ds_checklists[self._current_ds_id]:
589
            self._xccdf_store.append([xccdf_id])
590
591
    @async_action_wait
592
    def _update_profiles_store(self):
593
        """
594
        Clears and repopulates the store with profiles from the currently
595
        selected data stream and checklist.
596
597
        """
598
599
        if self._content_handler is None:
600
            # not initialized, cannot do anything
601
            return
602
603
        if self._using_ds and (
604
                self._current_xccdf_id is None or self._current_ds_id is None):
605
            # not initialized, cannot do anything
606
            return
607
608
        self._profiles_store.clear()
609
        try:
610
            profiles = self._content_handler.get_profiles()
611
        except scap_content_handler.SCAPContentHandlerError as e:
612
            log.warning("OSCAP Addon: " + str(e))
613
            self._invalid_content()
614
615
        for profile in profiles:
616
            profile_markup = '<span weight="bold">%s</span>\n%s' \
617
                                % (profile.title, profile.description)
618
            self._profiles_store.append([profile.id,
619
                                         profile_markup,
620
                                         profile.id == self._active_profile])
621
622
    def _add_message(self, message):
623
        """
624
        Add message to the store.
625
626
        :param message: message to be added
627
        :type message: org_fedora_oscap.common.RuleMessage
628
629
        """
630
631
        self._message_store.append([message.type, message.text])
632
633
    @async_action_wait
634
    def _update_message_store(self, report_only=False):
635
        """
636
        Updates the message store with messages from rule evaluation.
637
638
        :param report_only: wheter to do changes in configuration or just
639
                            report
640
        :type report_only: bool
641
642
        """
643
        if not self._policy_enabled:
644
            return
645
646
        self._message_store.clear()
647
648
        if not self._rule_data:
649
            # RuleData instance not initialized, cannot do anything
650
            return
651
652
        messages = self._rule_data.eval_rules(self.data, self._storage,
653
                                              report_only)
654
        if not messages:
655
            # no messages from the rules, add a message informing about that
656
            if not self._active_profile:
657
                # because of no profile
658
                message = common.RuleMessage(self.__class__,
659
                                             common.MESSAGE_TYPE_INFO,
660
                                             _("No profile selected"))
661
            else:
662
                # because of no pre-inst rules
663
                message = common.RuleMessage(self.__class__,
664
                                             common.MESSAGE_TYPE_INFO,
665
                                             _("No rules for the pre-installation phase"))
666
            self._add_message(message)
667
668
            # nothing more to be done
669
            return
670
671
        self._resolve_rootpw_issues(messages, report_only)
672
        for msg in messages:
673
            self._add_message(msg)
674
675
    def _resolve_rootpw_issues(self, messages, report_only):
676
        """Mitigate root password issues (which are not fatal in GUI)"""
677
        fatal_rootpw_msgs = [
678
            msg for msg in messages
679
            if msg.origin == rule_handling.PasswdRules and msg.type == common.MESSAGE_TYPE_FATAL]
680
681
        if fatal_rootpw_msgs:
682
            for msg in fatal_rootpw_msgs:
683
                # cannot just change the message type because it is a namedtuple
684
                messages.remove(msg)
685
686
                msg = common.RuleMessage(
687
                    self.__class__, common.MESSAGE_TYPE_WARNING, msg.text)
688
                messages.append(msg)
689
690
            passwords_can_be_fixed = False
691
            if not report_only and passwords_can_be_fixed:
692
                users_proxy = USERS.get_proxy()
693
694
                self.__old_root_pw = users_proxy.RootPassword
695
                self.data.rootpw.password = None
696
                self.__old_root_pw_seen = users_proxy.IsRootpwKickstarted
697
                self.data.rootpw.seen = False
698
699
    def _revert_rootpw_changes(self):
700
        if self.__old_root_pw is not None:
701
            users_proxy = USERS.get_proxy()
702
703
            users_proxy.SetRootPassword(self.__old_root_pw)
704
            self.__old_root_pw = None
705
706
            users_proxy.SetRootpwKickstarted(self.__old_root_pw_seen)
707
            self.__old_root_pw_seen = None
708
709
    @async_action_wait
710
    def _unselect_profile(self, profile_id):
711
        """Unselects the given profile."""
712
713
        if not profile_id:
714
            # no profile specified, nothing to do
715
            return
716
717
        itr = self._profiles_store.get_iter_first()
718
        while itr:
719
            if self._profiles_store[itr][0] == profile_id:
720
                self._profiles_store.set_value(itr, 2, False)
721
            itr = self._profiles_store.iter_next(itr)
722
723
        if self._rule_data:
724
            # revert changes and clear rule_data (no longer valid)
725
            self._rule_data.revert_changes(self.data, self._storage)
726
            self._revert_rootpw_changes()
727
            self._rule_data = None
728
729
        self._active_profile = ""
730
731
    @async_action_wait
732
    def _select_profile(self, profile_id):
733
        """Selects the given profile."""
734
735
        if not profile_id:
736
            # no profile specified, nothing to do
737
            return False
738
739
        ds = None
740
        xccdf = None
741
        if self._using_ds:
742
            ds = self._current_ds_id
743
            xccdf = self._current_xccdf_id
744
745
            if not all((ds, xccdf, profile_id)):
746
                # something is not set -> do nothing
747
                return False
748
749
        # get pre-install fix rules from the content
750
        try:
751
            self._rule_data = rule_handling.get_rule_data_from_content(
752
                profile_id, common.get_preinst_content_path(self._policy_data),
753
                ds, xccdf, common.get_preinst_tailoring_path(self._policy_data))
754
        except common.OSCAPaddonError as exc:
755
            log.error(
756
                "OSCAP Addon: Failed to get rules for the profile '{}': {}"
757
                .format(profile_id, str(exc)))
758
            self._set_error(
759
                "Failed to get rules for the profile '{}'"
760
                .format(profile_id))
761
            return False
762
763
        itr = self._profiles_store.get_iter_first()
764
        while itr:
765
            if self._profiles_store[itr][0] == profile_id:
766
                self._profiles_store.set_value(itr, 2, True)
767
            itr = self._profiles_store.iter_next(itr)
768
769
        # remember the active profile
770
        self._active_profile = profile_id
771
772
        return True
773
774
    @async_action_wait
775
    def _switch_profile(self):
776
        """Switches to a current selected profile.
777
778
        :returns: whether some profile was selected or not
779
780
        """
781
        if not self._policy_enabled:
782
            return False
783
784
        self._set_error(None)
785
        profile = self._current_profile_id
786
        if not profile:
787
            return False
788
789
        self._unselect_profile(self._active_profile)
790
        ret = self._select_profile(profile)
791
792
        # update messages according to the newly chosen profile
793
        self._update_message_store()
794
795
        return ret
796
797
    @set_ready
798
    def _set_error(self, msg):
799
        """Set or clear error message"""
800
        if msg:
801
            self._error = msg
802
            self.clear_info()
803
            self.set_error(msg)
804
        else:
805
            self._error = None
806
            self.clear_info()
807
808
    @async_action_wait
809
    def _general_content_problem(self):
810
        msg = _("There was an unexpected problem with the supplied content.")
811
        self._progress_label.set_markup("<b>%s</b>" % msg)
812
        self._wrong_content(msg)
813
814
    @async_action_wait
815
    def _invalid_content(self):
816
        """Callback for informing user about provided content invalidity."""
817
818
        msg = _("Invalid content provided. Enter a different URL, please.")
819
        self._progress_label.set_markup("<b>%s</b>" % msg)
820
        self._wrong_content(msg)
821
822
    @async_action_wait
823
    def _invalid_url(self):
824
        """Callback for informing user about provided URL invalidity."""
825
826
        msg = _("Invalid or unsupported content URL, please enter a different one.")
827
        self._progress_label.set_markup("<b>%s</b>" % msg)
828
        self._wrong_content(msg)
829
830
    @async_action_wait
831
    def _data_fetch_failed(self):
832
        """Adapts the UI if fetching data from entered URL failed"""
833
834
        msg = _("Failed to fetch content. Enter a different URL, please.")
835
        self._progress_label.set_markup("<b>%s</b>" % msg)
836
        self._wrong_content(msg)
837
838
    @async_action_wait
839
    def _network_problem(self):
840
        """Adapts the UI if network error was encountered during data fetch"""
841
842
        msg = _("Network error encountered when fetching data."
843
                " Please check that network is setup and working.")
844
        self._progress_label.set_markup("<b>%s</b>" % msg)
845
        self._wrong_content(msg)
846
847
    @async_action_wait
848
    def _integrity_check_failed(self):
849
        """Adapts the UI if integrity check fails"""
850
851
        msg = _("The integrity check of the content failed. Cannot use the content.")
852
        self._progress_label.set_markup("<b>%s</b>" % msg)
853
        self._wrong_content(msg)
854
855
    @async_action_wait
856
    def _extraction_failed(self, err_msg):
857
        """Adapts the UI if extracting data from entered URL failed"""
858
859
        msg = _("Failed to extract content (%s). Enter a different URL, "
860
                "please.") % err_msg
861
        self._progress_label.set_markup("<b>%s</b>" % msg)
862
        self._wrong_content(msg)
863
864
    @async_action_wait
865
    def _wrong_content(self, msg):
866
        content_discovery.clear_all(self._policy_data)
867
        really_hide(self._progress_spinner)
868
        self._fetch_button.set_sensitive(True)
869
        self._content_url_entry.set_sensitive(True)
870
        self._content_url_entry.grab_focus()
871
        self._content_url_entry.select_region(0, -1)
872
        self._set_error(msg)
873
874
    @async_action_wait
875
    def _invalid_profile_id(self):
876
        msg = _(
877
            "Profile with ID '%s' not defined in the content. "
878
            "Select a different profile, please"
879
        ) % self._policy_data.profile_id
880
881
        self._set_error(msg)
882
        self._policy_data.profile_id = ""
883
884
    @async_action_wait
885
    def _switch_dry_run(self, dry_run):
886
        self._choose_button.set_sensitive(not dry_run)
887
888
        if dry_run:
889
            # no profile can be selected in the dry-run mode
890
            self._unselect_profile(self._active_profile)
891
892
            # no messages in the dry-run mode
893
            self._message_store.clear()
894
            message = common.RuleMessage(self.__class__,
895
                                         common.MESSAGE_TYPE_INFO,
896
                                         _("Not applying security profile"))
897
            self._add_message(message)
898
899
            self._set_error(None)
900
        else:
901
            # mark the active profile as selected
902
            self._select_profile(self._active_profile)
903
            self._update_message_store()
904
905
    @async_action_wait
906
    def refresh(self):
907
        """
908
        The refresh method that is called every time the spoke is displayed.
909
        It should update the UI elements according to the contents of
910
        self.data.
911
912
        :see: pyanaconda.ui.common.UIObject.refresh
913
914
        """
915
        self._load_policy_data()
916
        # update the UI elements
917
        self._refresh_ui()
918
919
    def _refresh_ui(self):
920
        """Refresh the UI elements."""
921
        if not self._content_defined:
922
            log.info("OSCAP Addon: Content not defined")
923
            # hide the control buttons
924
            really_hide(self._control_buttons)
925
926
            # provide SSG if available
927
            if common.ssg_available():
928
                # show the SSG button and tweak the rest of the line
929
                # (the label)
930
                really_show(self._ssg_button)
931
                # TRANSLATORS: the other choice if SCAP Security Guide is also
932
                # available
933
                tip = _(" or enter data stream content or archive URL below:")
934
            else:
935
                # hide the SSG button
936
                really_hide(self._ssg_button)
937
                tip = _("No content found. Please enter data stream content or "
938
                        "archive URL below:")
939
940
            self._no_content_label.set_text(tip)
941
942
            # hide the progress box, no progress now
943
            with self._fetch_flag_lock:
944
                if not self._fetching:
945
                    really_hide(self._progress_box)
946
947
                    self._content_url_entry.set_sensitive(True)
948
                    self._fetch_button.set_sensitive(True)
949
950
                    if not self._content_url_entry.get_text():
951
                        # no text -> no info/warning
952
                        self._progress_label.set_text("")
953
954
            # switch to the page allowing user to enter content URL and fetch
955
            # it
956
            self._main_notebook.set_current_page(GET_CONTENT_PAGE)
957
            self._content_url_entry.grab_focus()
958
959
            # nothing more to do here
960
            return
961
        else:
962
            # show control buttons
963
            really_show(self._control_buttons)
964
965
            self._main_notebook.set_current_page(SET_PARAMS_PAGE)
966
967
        self._active_profile = self._policy_data.profile_id
968
969
        self._update_ids_visibility()
970
971
        if self._using_ds:
972
            if self._policy_data.datastream_id:
973
                set_combo_selection(self._ds_combo,
974
                                    self._policy_data.datastream_id,
975
                                    unset_first=True)
976
            else:
977
                try:
978
                    default_ds = next(iter(self._ds_checklists.keys()))
979
                    set_combo_selection(self._ds_combo, default_ds,
980
                                        unset_first=True)
981
                except StopIteration:
982
                    # no data stream available
983
                    pass
984
985
                if self._policy_data.datastream_id and self._policy_data.xccdf_id:
986
                    set_combo_selection(self._xccdf_combo,
987
                                        self._policy_data.xccdf_id,
988
                                        unset_first=True)
989
        else:
990
            # no combobox changes --> need to update profiles store manually
991
            self._update_profiles_store()
992
993
        if self._policy_data.profile_id:
994
            set_treeview_selection(self._profiles_view,
995
                                   self._policy_data.profile_id)
996
997
        self._update_message_store()
998
999
    def apply(self):
1000
        """
1001
        The apply method that is called when the spoke is left. It should
1002
        update the contents of self.data with values set in the GUI elements.
1003
1004
        """
1005
1006
        if not self._content_defined or not self._active_profile:
1007
            # no errors for no content or no profile
1008
            self._set_error(None)
1009
1010
        # store currently selected values to the addon data attributes
1011
        if self._using_ds:
1012
            self._policy_data.datastream_id = self._current_ds_id
1013
            self._policy_data.xccdf_id = self._current_xccdf_id
1014
1015
        self._policy_data.profile_id = self._active_profile
1016
        self._policy_enabled = self._dry_run_switch.get_active()
1017
1018
        # set up the DBus module
1019
        self._oscap_module.PolicyData = PolicyData.to_structure(self._policy_data)
1020
        self._oscap_module.PolicyEnabled = self._policy_enabled
1021
1022
    def execute(self):
1023
        """
1024
        The excecute method that is called when the spoke is left. It is
1025
        supposed to do all changes to the runtime environment according to
1026
        the values set in the GUI elements.
1027
1028
        """
1029
1030
        # nothing to do here
1031
        pass
1032
1033
    @property
1034
    def ready(self):
1035
        """
1036
        The ready property that tells whether the spoke is ready (can be
1037
        visited) or not.
1038
1039
        :rtype: bool
1040
1041
        """
1042
1043
        return self._ready
1044
1045
    @property
1046
    def completed(self):
1047
        """
1048
        The completed property that tells whether all mandatory items on the
1049
        spoke are set, or not. The spoke will be marked on the hub as completed
1050
        or uncompleted acording to the returned value.
1051
1052
        :rtype: bool
1053
1054
        """
1055
1056
        # no error message in the store
1057
        return not self._error and all(row[0] != common.MESSAGE_TYPE_FATAL
1058
                                       for row in self._message_store)
1059
1060
    @property
1061
    @async_action_wait
1062
    def status(self):
1063
        """
1064
        The status property that is a brief string describing the state of the
1065
        spoke. It should describe whether all values are set and if possible
1066
        also the values themselves. The returned value will appear on the hub
1067
        below the spoke's title.
1068
1069
        :rtype: str
1070
1071
        """
1072
1073
        if self._error:
1074
            return _("Error fetching and loading content")
1075
1076
        if self._unitialized_status:
1077
            # not initialized
1078
            return self._unitialized_status
1079
1080
        if not self._content_defined:
1081
            return _("No content found")
1082
1083
        if not self._active_profile:
1084
            return _("No profile selected")
1085
1086
        # update message store, something may changed from the last update
1087
        self._update_message_store(report_only=True)
1088
1089
        warning_found = False
1090
        for row in self._message_store:
1091
            if row[0] == common.MESSAGE_TYPE_FATAL:
1092
                return _("Misconfiguration detected")
1093
            elif row[0] == common.MESSAGE_TYPE_WARNING:
1094
                warning_found = True
1095
1096
        # TODO: at least the last two status messages need a better wording
1097
        if warning_found:
1098
            return _("Warnings appeared")
1099
1100
        return _("Everything okay")
1101
1102
    def on_ds_combo_changed(self, *args):
1103
        """Handler for the datastream ID change."""
1104
1105
        ds_id = self._current_ds_id
1106
        if not ds_id:
1107
            return
1108
1109
        self._update_xccdfs_store()
1110
        first_checklist = self._ds_checklists[ds_id][0]
1111
1112
        set_combo_selection(self._xccdf_combo, first_checklist)
1113
1114
    def on_xccdf_combo_changed(self, *args):
1115
        """Handler for the XCCDF ID change."""
1116
        self._content_handler.select_checklist(
1117
            self._current_ds_id, self._current_xccdf_id)
1118
1119
        # may take a while
1120
        self._update_profiles_store()
1121
1122
    def on_profiles_selection_changed(self, *args):
1123
        """Handler for the profile selection change."""
1124
        if not self._policy_enabled:
1125
            return
1126
1127
        cur_profile = self._current_profile_id
1128
        if cur_profile:
1129
            if cur_profile != self._active_profile:
1130
                # new profile selected, make the selection button sensitive
1131
                self._choose_button.set_sensitive(True)
1132
            else:
1133
                # current active profile selected
1134
                self._choose_button.set_sensitive(False)
1135
1136
    def on_profile_clicked(self, widget, event, *args):
1137
        """Handler for the profile being clicked on."""
1138
        if not self._policy_enabled:
1139
            return
1140
1141
        # if a profile is double-clicked, we should switch to it
1142
        # pylint: disable = E1101
1143
        if event.type == Gdk.EventType._2BUTTON_PRESS:
1144
            self._switch_profile()
1145
1146
            # active profile selected
1147
            self._choose_button.set_sensitive(False)
1148
1149
        # let the other actions hooked to the click happen as well
1150
        return False
1151
1152
    def on_profile_chosen(self, *args):
1153
        """
1154
        Handler for the profile being chosen
1155
        (e.g. "Select profile" button hit).
1156
1157
        """
1158
1159
        # switch profile
1160
        self._switch_profile()
1161
1162
        # active profile selected
1163
        self._choose_button.set_sensitive(False)
1164
1165
    def on_fetch_button_clicked(self, *args):
1166
        """Handler for the Fetch button"""
1167
1168
        with self._fetch_flag_lock:
1169
            if self._fetching:
1170
                # some other fetching/pre-processing running, give up
1171
                log.warn(
1172
                    "OSCAP Addon: "
1173
                    "Clicked the fetch button, although the GUI is in the fetching mode.")
1174
                return
1175
1176
        # prevent user from changing the URL in the meantime
1177
        self._content_url_entry.set_sensitive(False)
1178
        self._fetch_button.set_sensitive(False)
1179
        url = self._content_url_entry.get_text()
1180
        really_show(self._progress_box)
1181
        really_show(self._progress_spinner)
1182
1183
        if not data_fetch.can_fetch_from(url):
1184
            msg = _("Invalid or unsupported URL")
1185
            # cannot start fetching
1186
            self._progress_label.set_markup("<b>%s</b>" % msg)
1187
            self._wrong_content(msg)
1188
            return
1189
1190
        self._progress_label.set_text(_("Fetching content..."))
1191
        self._progress_spinner.start()
1192
        self._policy_data.content_url = url
1193
        if url.endswith(".rpm"):
1194
            self._policy_data.content_type = "rpm"
1195
        elif any(url.endswith(arch_type) for arch_type in common.SUPPORTED_ARCHIVES):
1196
            self._policy_data.content_type = "archive"
1197
        else:
1198
            self._policy_data.content_type = "datastream"
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
        content_discovery.clear_all(self._policy_data)
1210
        self._refresh_ui()
1211
1212
    def on_use_ssg_clicked(self, *args):
1213
        self.content_bringer.use_system_content()
1214
        self._save_policy_data()
1215
        self._fetch_data_and_initialize()
1216