Passed
Pull Request — master (#175)
by
unknown
02:22 queued 01:19
created

OSCAPSpoke.on_profile_chosen()   A

Complexity

Conditions 1

Size

Total Lines 12
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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