#!/usr/bin/python3

# 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 *
from machine_core.constants import TEST_OS_DEFAULT


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
"""


class TestSelinux(MachineCase):
    provision = {
        "0": {},
        "ansible_machine": {"image": TEST_OS_DEFAULT}
    }

    @skipImage("No setroubleshoot", "debian-stable", "debian-testing", "fedora-coreos", "ubuntu-1804", "ubuntu-2004", "ubuntu-stable")
    @skipBrowser("Firefox needs http to access clipboard", "firefox")
    def testTroubleshootAlerts(self):
        b = self.browser
        m = self.machine

        # Do some local modifications
        m.execute("semanage fcontext --add -t samba_share_t /var/tmp/")
        m.execute("semanage boolean --modify --on zebra_write_config")
        self.allow_journal_messages("audit: type=1405 .* bool=zebra_write_config val=1 old_val=0.*")
        self.allow_journal_messages(".*couldn't introspect /org/fedoraproject/Setroubleshootd: GDBus.Error:org.freedesktop.DBus.Error.NoReply: Remote peer disconnected.*")
        self.allow_authorize_journal_messages()

        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.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_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)

        if self.machine.image == "rhel-8-2-distropkg":
            b.wait_present(row_selector + " button.pficon-delete")
        else:
            b.wait_present(row_selector + " #selinux-alert-dismiss")

        # 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.click(toggle_selector)

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

        try:
            # a solution is present
            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{}".format(self.default_btn_class)
            b.click(btn_sel)

            # the fix should be applied
            b.wait_in_text(row_selector + " .pf-c-alert__title", "Solution applied successfully")
        except Error:
            print("==== sealert -l '*' ====")
            print(m.execute("sealert -l '*'"))
            print("==== audit.log  ====")
            print(m.execute("cat /var/log/audit/audit.log"))
            print("====================")
            raise

        b.click(row_selector + " button{}".format(self.danger_btn_class))
        b.wait_not_present(row_selector)

        b.grant_permissions("clipboardRead", "clipboardWrite")

        b.wait_visible("tr.modification-row > td:contains(Allow zebra to write config)")
        b.wait_visible("tr.modification-row > td:contains(fcontext -a -f a -t samba_share_t -r 's0' '/var/tmp/')")
        b.click(".modifications-export")
        shell_script_sel = ".automation-script-modal .modal-body section:nth-child(2) pre"
        ansible_script_sel = ".automation-script-modal .modal-body section:nth-child(3) pre"
        b.wait_in_text(shell_script_sel, "boolean -D")
        b.wait_in_text(shell_script_sel, "fcontext -D")
        b.wait_in_text(shell_script_sel, "boolean -m -1 zebra_write_config")
        b.wait_in_text(shell_script_sel, "fcontext -a -f a -t samba_share_t -r 's0' '/var/tmp/'")

        # Check ansible
        def normalize(script):
            # HACK: `permissive` type is exported only since policycoreutils 3.0
            # See https://github.com/SELinuxProject/selinux/commit/3a9b4505b
            # Combining of versions < 3.0 with versions >= 3.0 provides a bit
            # different outputs.
            return script.replace("permissive -D\n", "")


        b.click("button:contains('Ansible')")
        b.wait_in_text(ansible_script_sel, "- name: Allow zebra to write config")

        ansible_m = self.machines["ansible_machine"]
        ansible_m.execute("mkdir -p roles/selinux/tasks/")
        ansible_m.write("tests.yml",
"""
- hosts: localhost
  connection: local
  roles:
    - selinux
""")
        ansible_m.write("roles/selinux/tasks/main.yml", b.text(ansible_script_sel))
        se_before = normalize(ansible_m.execute("semanage export"))
        ansible_m.execute("ansible-playbook tests.yml")
        se_after = normalize(ansible_m.execute("semanage export"))
        local = normalize(m.execute("semanage export"))

        self.assertNotEqual(se_before, se_after)
        self.assertEqual(local, se_after)

        # Check that ansible is idempotent
        m.execute("semanage boolean --modify --off zebra_write_config")
        b.reload()
        b.enter_page("/selinux/setroubleshoot")
        b.wait_visible("tr.modification-row > td:contains(Disallow zebra to write config)")
        b.click(".modifications-export")
        b.click("button:contains('Ansible')")
        ansible_m.write("roles/selinux/tasks/main.yml", b.text(ansible_script_sel))
        se_before = se_after
        ansible_m.execute("ansible-playbook tests.yml")
        se_after = normalize(ansible_m.execute("semanage export"))
        local = normalize(m.execute("semanage export"))
        self.assertNotEqual(se_before, se_after)
        self.assertEqual(local, se_after)

        # Check the content of clipboard by pasting the clipboard to the terminal
        b.click("button:contains('Shell Script')")
        b.wait_in_text(shell_script_sel, "boolean -D")
        b.click(".automation-script-modal .fa-clipboard")
        b.go("/system/terminal")
        b.enter_page("/system/terminal")
        b.focus('.terminal')
        b.key_press("\"\r")
        # Right click and paste
        b.mouse(".terminal", "contextmenu", btn=2)
        b.click('.contextMenu .contextMenuOption:nth-child(2)')
        b.wait_in_text(".xterm-accessibility-tree", "semanage import <<EOF")
        b.wait_in_text(".xterm-accessibility-tree", "boolean -D")
        b.wait_in_text(".xterm-accessibility-tree", "boolean -m -0 zebra_write_config")
        b.wait_in_text(".xterm-accessibility-tree", "fcontext -a -f a -t samba_share_t -r 's0' '/var/tmp/'")

        m.execute("semanage boolean -D && semanage fcontext -D")
        b.go("/selinux/setroubleshoot")
        b.enter_page("/selinux/setroubleshoot")
        b.reload()
        b.enter_page("/selinux/setroubleshoot")
        b.wait_text("table.listing-ct.modifications-table > thead > tr > td", "No System Modifications")
        b.relogin("/selinux/setroubleshoot", "admin", authorized=False)
        b.wait_text("table.listing-ct.modifications-table > thead > tr > td", "The logged in user is not permitted to view system modifications")


    @skipImage("No cockpit-selinux", "debian-stable", "debian-testing", "fedora-coreos", "ubuntu-1804", "ubuntu-2004", "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")

        def assertEnforce(active):
            b.wait_present(".onoff-ct input" + (active and ":checked" or ":not(:checked)"))

        # SELinux should be enabled and enforcing at the beginning
        assertEnforce(True)
        m.execute("getenforce | grep -q 'Enforcing'")

        # now set to permissive using the ui button
        b.click(".onoff-ct input")
        assertEnforce(False)
        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")
        assertEnforce(True)

        # 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")
        assertEnforce(False)
        b.wait_in_text("div.selinux-policy-ct", "Setting deviates")


if __name__ == '__main__':
    test_main()
