Passed
Pull Request — master (#175)
by
unknown
01:07
created

OSCAPSpoke.get_screen_id()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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