Passed
Pull Request — master (#169)
by
unknown
01:51
created

OSCAPSpoke.__init__()   A

Complexity

Conditions 1

Size

Total Lines 56
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 21
nop 4
dl 0
loc 56
rs 9.376
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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