#!/usr/bin/python3
# -*- coding: utf-8 -*-

# This file is part of Cockpit.
#
# Copyright (C) 2016 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import parent
from testlib import *

FIX_AUDITD = """
set -e
mkdir -p /var/log/audit
touch /var/log/audit.log
restorecon -r /var/log/audit
systemctl start auditd
"""

SELINUX_ALERT_SCRIPT = """
set -e
mkdir -p ~/selinux_temp
cd ~/selinux_temp
cp /bin/ls ls
chcon -t httpd_exec_t ls
# this won't work, but it generates an error
runcon -u system_u -r system_r -t httpd_t -- ./ls  /home/* || true
"""

SELINUX_FIXABLE_ALERT_SCRIPT = """
set -e
mkdir -p ~/.ssh
ssh-keygen -t rsa -f ~/.ssh/test-avc-rsa -N ""
mv -f ~/.ssh/authorized_keys ~/.ssh/authorized_keys.test-avc
cat .ssh/test-avc-rsa.pub >> ~/.ssh/authorized_keys
chcon -t httpd_exec_t ~/.ssh/authorized_keys
auditctl -D
auditctl -w ~/.ssh/authorized_keys -p a
ssh -o StrictHostKeyChecking=no -o 'BatchMode=yes' -i ~/.ssh/test-avc-rsa localhost || true
mv -f ~/.ssh/authorized_keys.test-avc ~/.ssh/authorized_keys
"""

@skipImage("DBus API of setroubleshoot too old", "centos-7")
class TestSelinux(MachineCase):
    @skipImage("No setroubleshoot", "continuous-atomic", "debian-stable", "debian-testing", "fedora-atomic", "rhel-atomic", "ubuntu-1604", "ubuntu-stable")
    def testTroubleshootAlerts(self):
        b = self.browser

        self.login_and_go("/selinux/setroubleshoot")

        # at the beginning, there should be no entries
        b.wait_in_text("body", "No SELinux alerts.")

        # fix audit events first
        self.machine.execute(script=FIX_AUDITD)

        #########################################################
        # trigger an alert
        self.machine.execute(script=SELINUX_ALERT_SCRIPT)

        row_selector = "tbody:contains('ls from read access on the directory')"

        # wait for the alert to appear
        b.wait_present(row_selector)

        # expand it to see details
        toggle_selector = row_selector + " .listing-ct-toggle"
        b.wait_present(toggle_selector)
        b.click(toggle_selector)

        # this should have two alerts, but there seems to be a known issue that some messages are delayed
        # b.wait_in_text(row_selector, "2 occurrences")

        panel_selector = row_selector + " tr.listing-ct-panel"

        # a solution is present
        b.wait_present(panel_selector)
        b.wait_in_text(panel_selector, "You must tell SELinux about this by enabling the 'httpd_read_user_content' boolean.")

        # ... but we can't apply it automatically
        # there are several solutions this can match against - that's ok, we want at least one to have this message
        b.wait_in_text(panel_selector, "Unable to apply this solution automatically")

        # collapse again
        b.click(toggle_selector)

        # wait until it is collapsed
        b.wait_not_in_text(row_selector, "You must tell SELinux about this by enabling the 'httpd_read_user_content' boolean.")

        # now expand again so we can dismiss the alert
        b.click(toggle_selector)
        b.wait_present(row_selector + " button.pficon-delete")

        # HACK: we need updated test images for delete to work
        # dismiss the alert
        #b.click(row_selector + " button.pficon-delete")
        #b.wait_not_present(row_selector)

        #b.wait_in_text("body", "No SELinux alerts.")

        #########################################################
        # trigger a fixable alert
        self.machine.execute(script=SELINUX_FIXABLE_ALERT_SCRIPT)

        row_selector = "tbody:contains('read access on the file /root/.ssh/authorized_keys')"

        # wait for the alert to appear
        b.wait_present(row_selector)

        # expand it to see details
        toggle_selector = row_selector + " .listing-ct-toggle"
        b.wait_present(toggle_selector)
        b.click(toggle_selector)

        panel_selector = row_selector + " tr.listing-ct-panel"

        # a solution is present
        b.wait_present(panel_selector)
        b.wait_in_text(panel_selector, "you can run restorecon")

        # and it can be applied
        btn_sel = "div.list-group-item:contains('you can run restorecon') button"
        b.click(btn_sel)

        # the fix should be applied
        b.wait_in_text(row_selector, "Solution applied successfully:")

        b.click(row_selector + " button.pficon-delete")
        b.wait_not_present(row_selector)

    @skipImage("No cockpit-selinux", "debian-stable", "debian-testing", "continuous-atomic", "fedora-atomic", "ubuntu-1604", "ubuntu-stable")
    def testEnforcingToggle(self):
        b = self.browser
        m = self.machine

        # HACK: http://bugzilla.redhat.com/show_bug.cgi?id=1346364
        # This issue happens when selinux item is in kickstart
        # as happens in our virt-builder and virt-install usage
        m.execute("chmod ugo+r /etc/selinux/config")

        self.login_and_go("/selinux/setroubleshoot")

        #########################################################
        # wait for the page to be present
        b.wait_in_text("body", "SELinux Policy")

        # SELinux should be enabled and enforcing at the beginning
        on_off_selector_active = "div.btn-onoff-ct label.active"
        b.wait_in_text(on_off_selector_active, "On")
        m.execute("getenforce | grep -q 'Enforcing'")

        # now set to permissive using the ui button
        b.click("div.btn-onoff-ct label.active span")
        b.wait_in_text(on_off_selector_active, "Off")
        m.execute("getenforce | grep -q 'Permissive'")

        # when in permissive mode, expect a warning
        b.wait_in_text("div.selinux-policy-ct", "Setting deviates")

        # switch back using cli, ui should react
        m.execute("setenforce 1")
        b.wait_in_text(on_off_selector_active, "On")

        # warning should disappear
        b.wait_not_in_text("div.selinux-policy-ct", "Setting deviates")

        # Switch to another page
        b.switch_to_top()
        b.go("/system")
        b.enter_page("/system")

        # Now on another page change the status
        m.execute("setenforce 0")

        # Switch back into the page and we get the updated status
        b.switch_to_top()
        b.go("/selinux/setroubleshoot")
        b.enter_page("/selinux/setroubleshoot")
        b.wait_in_text(on_off_selector_active, "Off")
        b.wait_in_text("div.selinux-policy-ct", "Setting deviates")

if __name__ == '__main__':
    test_main()
