Completed
Push — master ( 0db8a8...79f3e5 )
by Matěj
18s queued 13s
created

OSCAPSpoke.ready()   A

Complexity

Conditions 1

Size

Total Lines 11
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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