Completed
Push — master ( d462aa...30f501 )
by
unknown
11s
created

OSCAPSpoke._invalid_url()   A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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