#!/usr/bin/python3

# This file is part of Cockpit.
#
# Copyright (C) 2020 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 *


def allow_old_cockpit_ws_messsages(test):
    # noisy debug message from old cockpit-ws/polkit
    test.allow_journal_messages("logged in user session",
                                "New connection to session from.*",
                                "pam_unix(polkit-1:session): session opened for user root by .*uid=.*",
                                "admin: Executing command .*COMMAND=/usr/bin/cockpit-bridge --privileged.*")


@skipDistroPackage()
class TestSuperuser(MachineCase):
    def testBasic(self):
        b = self.browser
        m = self.machine

        # Log in with all defaults
        self.login_and_go()
        b.check_superuser_indicator("Administrative access")

        b.assert_pixels("#topnav", "topnav-privileged")

        # Drop privileges
        b.open_superuser_dialog()
        b.click(".pf-c-modal-box:contains('Switch to limited access') button:contains('Limit access')")
        b.wait_not_present(".pf-c-modal-box:contains('Switch to limited access')")
        b.check_superuser_indicator("Limited access")

        b.assert_pixels("#topnav", "topnav-unprivileged")

        # Check they are still gone after logout
        b.relogin()
        b.leave_page()
        b.check_superuser_indicator("Limited access")

        # We want to be lectured again
        m.execute("rm -rf /var/{db,lib}/sudo/lectured/admin")

        # Get the privileges back, this time in the mobile layout
        b.set_layout("mobile")
        b.open_superuser_dialog()
        if "ubuntu" not in m.image:
            b.wait_in_text(".pf-c-modal-box:contains('Switch to administrative access')",
                           "We trust you have received the usual lecture")
        b.wait_in_text(".pf-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
        b.set_input_text(".pf-c-modal-box:contains('Switch to administrative access') input", "foobar")
        b.focus(".pf-c-modal-box button:contains('Cancel')")
        b.assert_pixels(".pf-c-modal-box:contains('Switch to administrative access')", "superuser-modal")
        b.click(".pf-c-modal-box button:contains('Authenticate')")
        b.wait_not_present(".pf-c-modal-box:contains('Switch to administrative access')")
        b.check_superuser_indicator("Administrative access")
        b.set_layout("desktop")

        # Check we still have them after logout
        b.relogin()
        b.leave_page()
        b.check_superuser_indicator("Administrative access")

    def testNoPasswd(self):
        b = self.browser
        m = self.machine

        # Log in with limited access
        self.login_and_go(superuser=False)
        b.check_superuser_indicator("Limited access")

        # Give us password-less sudo and use it
        m.write("/etc/sudoers.d/admin", "admin ALL=(ALL) NOPASSWD:ALL")
        b.open_superuser_dialog()
        b.wait_in_text(".pf-c-modal-box:contains('Administrative access')", "You now have administrative access")
        b.click(".pf-c-modal-box:contains('Administrative access') button:contains('Close')")
        b.wait_not_present(".pf-c-modal-box:contains('Administrative access')")
        b.check_superuser_indicator("Administrative access")

    def testTwoPasswds(self):
        b = self.browser
        m = self.machine

        # Log in with limited access
        self.login_and_go(superuser=False)
        b.check_superuser_indicator("Limited access")

        # Configure the sudo PAM stack to make two prompts
        if "debian" in m.image or "ubuntu" in m.image:
            m.write("/etc/pam.d/sudo", """
auth required pam_unix.so
auth required /usr/lib/cockpit-test-assets/mock-pam-conv-mod.so
@include common-account
@include common-session-noninteractive
""")
        else:
            m.write("/etc/pam.d/sudo", """
auth required pam_unix.so
auth required /usr/lib/cockpit-test-assets/mock-pam-conv-mod.so
account include system-auth
password include system-auth
session include system-auth
""")

        b.open_superuser_dialog()
        b.wait_in_text(".pf-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
        b.set_input_text(".pf-c-modal-box:contains('Switch to administrative access') input", "foobar")
        b.click(".pf-c-modal-box button:contains('Authenticate')")
        b.wait_in_text(".pf-c-modal-box:contains('Switch to administrative access')", "universe and everything")
        b.set_input_text(".pf-c-modal-box:contains('Switch to administrative access') input", "42")
        b.click(".pf-c-modal-box button:contains('Authenticate')")
        b.wait_not_present(".pf-c-modal-box:contains('Switch to administrative access')")
        b.check_superuser_indicator("Administrative access")

    def testWrongPasswd(self):
        b = self.browser

        # Log in with limited access
        self.login_and_go(superuser=False)
        b.check_superuser_indicator("Limited access")

        b.open_superuser_dialog()
        b.wait_in_text(".pf-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
        b.set_input_text(".pf-c-modal-box:contains('Switch to administrative access') input", "wrong")
        b.click(".pf-c-modal-box button:contains('Authenticate')")
        b.wait_in_text(".pf-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
        b.wait_in_text(".pf-c-modal-box:contains('Switch to administrative access')", "Sorry, try again")
        b.set_input_text(".pf-c-modal-box:contains('Switch to administrative access') input", "wronger")
        b.click(".pf-c-modal-box button:contains('Authenticate')")
        b.wait_in_text(".pf-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
        b.wait_in_text(".pf-c-modal-box:contains('Switch to administrative access')", "Sorry, try again")
        b.set_input_text(".pf-c-modal-box:contains('Switch to administrative access') input", "wrongest")
        b.click(".pf-c-modal-box button:contains('Authenticate')")
        b.wait_in_text(".pf-c-modal-box:contains('Problem becoming administrator')", "Sudo: 3 incorrect password attempts")
        b.click(".pf-c-modal-box:contains('Problem becoming administrator') button:contains('Close')")
        b.wait_not_present(".pf-c-modal-box:contains('Problem becoming administrator')")
        b.check_superuser_indicator("Limited access")

    def testNotAdmin(self):
        b = self.browser
        m = self.machine

        # Remove special treatment of the "admin" group on Ubuntu.
        # Our main test user is unfortunately called "admin" and has
        # "admin" as its primary group.
        #
        if "ubuntu" in m.image:
            m.execute("sed -i -e '/^%admin/d' /etc/sudoers")

        m.execute(f"gpasswd -d admin {m.get_admin_group()}")

        self.login_and_go()
        b.check_superuser_indicator("Limited access")

        b.open_superuser_dialog()
        b.set_input_text(".pf-c-modal-box:contains('Switch to administrative access') input", "foobar")
        b.click(".pf-c-modal-box button:contains('Authenticate')")
        b.wait_in_text(".pf-c-modal-box:contains('Problem becoming administrator')", "Admin is not in the sudoers file.  This incident will be reported.")
        b.click(".pf-c-modal-box:contains('Problem becoming administrator') button:contains('Close')")
        b.wait_not_present(".pf-c-modal-box:contains('Problem becoming administrator')")

    def testBrokenBridgeConfig(self):
        b = self.browser
        m = self.machine

        m.needs_writable_usr()
        m.write("/usr/share/cockpit/shell/override.json", """
{
  "bridges": [
    {
      "privileged": true,
      "spawn": [
        "sudo",
        "-A",
        "cockpit-bridge",
        "--privileged"
      ]
    }
  ]
}
""")

        # We don't want to be lectured in this test just to control
        # the content of the dialog better.
        m.execute("touch /var/{db,lib}/sudo/lectured/admin 2>/dev/null || true")

        self.login_and_go(superuser=False)
        b.check_superuser_indicator("Limited access")

        b.open_superuser_dialog()
        b.wait_in_text(".pf-c-modal-box:contains('Problem becoming administrator')", "Sudo: no askpass program specified, try setting SUDO_ASKPASS")
        b.click(".pf-c-modal-box:contains('Problem becoming administrator') button:contains('Close')")
        b.wait_not_present(".pf-c-modal-box:contains('Problem becoming administrator')")
        b.check_superuser_indicator("Limited access")

    def testRemoveBridgeConfig(self):
        b = self.browser
        m = self.machine

        m.needs_writable_usr()

        self.login_and_go("/playground/pkgs", superuser=True)
        b.leave_page()
        b.check_superuser_indicator("Administrative access")

        m.write("/usr/share/cockpit/shell/override.json", """
{
  "bridges": [ ]
}
""")

        b.enter_page("/playground/pkgs")
        b.click("#reload")
        b.leave_page()
        b.check_superuser_indicator("Limited access")

    def testOverview(self):
        b = self.browser

        self.login_and_go("/system", superuser=False)
        b.wait_visible(".pf-c-alert:contains('Web console is running in limited access mode.')")
        b.click(".pf-c-alert:contains('Web console is running in limited access mode.') button:contains('Turn on')")
        b.wait_in_text(".pf-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
        b.set_input_text(".pf-c-modal-box:contains('Switch to administrative access') input", "foobar")
        b.click(".pf-c-modal-box button:contains('Authenticate')")
        b.wait_not_present(".pf-c-modal-box:contains('Switch to administrative access')")
        b.wait_not_visible(".pf-c-alert:contains('Web console is running in limited access mode.')")


@skipDistroPackage()
class TestSuperuserOldShell(MachineCase):
    provision = {
        "machine1": {"address": "10.111.113.1/20"},
        "machine2": {"address": "10.111.113.2/20", "image": "centos-7"},
    }

    def test(self):
        b = self.browser
        m = self.machine

        m.start_cockpit()

        # Use m1 to login into m2
        b.open("/")
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "foobar")
        b.click("#show-other-login-options")
        b.wait_visible("#server-group")
        b.set_val("#server-field", "10.111.113.2")
        b.click('#login-button')
        b.wait_in_text("#server-name", "10.111.113.2")
        b.wait_visible("#hostkey-group")
        b.wait_in_text("#hostkey-message-1", "You are connecting to 10.111.113.2 for the first time.")
        b.click('#login-button')
        b.expect_load()
        b.wait_visible('#content')
        b.wait_visible('#content')

        # The old shell should have gotten the password from cockpit-ws and it should work
        b.enter_page("/system")
        b.click("#shutdown-group > button:contains('Restart')")
        b.wait_popup("shutdown-dialog")
        b.click("#shutdown-dialog button:contains('Restart')")
        b.wait_popdown("shutdown-dialog")


@skipImage("TODO: broken on Arch Linux", "arch")
@skipDistroPackage()
class TestSuperuserOldWebserver(MachineCase):
    provision = {
        "machine1": {"address": "10.111.113.1/20", "image": "centos-7"},
        "machine2": {"address": "10.111.113.2/20"},
    }

    def test(self):
        b = self.browser
        m = self.machine

        m.execute("firewall-cmd --add-service cockpit")
        m.start_cockpit()

        # Use m1 to login into m2
        b.open("/")
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "foobar")
        b.set_checked('#authorized-input', True)
        b.click("#show-other-login-options")
        b.wait_visible("#server-group")
        b.set_val("#server-field", "10.111.113.2")
        b.click('#login-button')
        b.wait_in_text("#server-name", "10.111.113.2")
        b.wait_visible("#conversation-group")
        b.wait_in_text("#conversation-prompt", "Fingerprint")
        b.wait_in_text("#conversation-message", "Do you want to proceed this time?")
        b.click('#login-button')
        b.expect_load()
        b.wait_visible('#content')
        b.wait_visible('#content')

        # We should have gotten the password from the old cockpit-ws and it should work
        b.check_superuser_indicator("Administrative access")
        b.go("/playground/test")
        b.enter_page("/playground/test")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'result: ')
        self.assertIn('result: uid=0', b.text(".super-channel span"))

        allow_old_cockpit_ws_messsages(self)

    def testNotAuth(self):
        b = self.browser
        m = self.machine

        m.execute("firewall-cmd --add-service cockpit")
        m.start_cockpit()

        # Use m1 to login into m2, but don't reuse the password
        b.open("/")
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "foobar")
        b.set_checked('#authorized-input', False)
        b.click("#show-other-login-options")
        b.wait_visible("#server-group")
        b.set_val("#server-field", "10.111.113.2")
        b.click('#login-button')
        b.wait_in_text("#server-name", "10.111.113.2")
        b.wait_visible("#conversation-group")
        b.wait_in_text("#conversation-prompt", "Fingerprint")
        b.wait_in_text("#conversation-message", "Do you want to proceed this time?")
        b.click('#login-button')
        b.expect_load()
        b.wait_visible('#content')
        b.wait_visible('#content')

        # We should not have gotten the password from the old
        # cockpit-ws, but we can get it back.

        b.check_superuser_indicator("Limited access")
        b.go("/playground/test")
        b.enter_page("/playground/test")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'access-denied')

        b.switch_to_top()
        b.open_superuser_dialog()
        b.wait_in_text(".pf-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
        b.set_input_text(".pf-c-modal-box:contains('Switch to administrative access') input", "foobar")
        b.click(".pf-c-modal-box button:contains('Authenticate')")
        b.wait_not_present(".pf-c-modal-box:contains('Switch to administrative access')")
        b.check_superuser_indicator("Administrative access")
        b.go("/playground/test")
        b.enter_page("/playground/test")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'result: ')
        self.assertIn('result: uid=0', b.text(".super-channel span"))

        allow_old_cockpit_ws_messsages(self)


@skipDistroPackage()
class TestSuperuserDashboard(MachineCase):
    provision = {
        "machine1": {"address": "10.111.113.1/20"},
        "machine2": {"address": "10.111.113.2/20"},
    }

    def test(self):
        b = self.browser

        self.login_and_go()
        b.go("/@10.111.113.2")
        b.wait_visible("#machine-troubleshoot")
        b.click('#machine-troubleshoot')
        b.wait_visible('#hosts_setup_server_dialog')
        b.wait_visible('#hosts_setup_server_dialog button:contains("Add")')
        b.click('#hosts_setup_server_dialog button:contains("Add")')
        b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")
        b.click('#hosts_setup_server_dialog button:contains("Accept key and connect")')
        b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")
        b.set_input_text("#login-custom-password", "foobar")
        b.click('#hosts_setup_server_dialog button:contains("Log in")')
        b.wait_not_present('#hosts_setup_server_dialog')

        # There should be no superuser indicator in the Overview

        b.go("/@10.111.113.2/system")
        b.enter_page("/system", host="10.111.113.2")
        b.wait_not_present(".ct-overview-header-actions button:contains('Administrative access')")
        b.wait_not_present(".ct-overview-header-actions button:contains('Limited access')")
        b.leave_page()

        # The superuser indicator in the Shell should apply to machine2

        b.check_superuser_indicator("Administrative access")
        b.go("/@10.111.113.2/playground/test")
        b.enter_page("/playground/test", host="10.111.113.2")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'result: ')
        self.assertIn('result: uid=0', b.text(".super-channel span"))

        b.leave_page()
        b.open_superuser_dialog()
        b.click(".pf-c-modal-box:contains('Switch to limited access') button:contains('Limit access')")
        b.wait_not_present(".pf-c-modal-box:contains('Switch to limited access')")
        b.check_superuser_indicator("Limited access")
        b.enter_page("/playground/test", host="10.111.113.2")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'access-denied')

        b.leave_page()
        b.open_superuser_dialog()
        b.wait_in_text(".pf-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
        b.set_input_text(".pf-c-modal-box:contains('Switch to administrative access') input", "foobar")
        b.click(".pf-c-modal-box button:contains('Authenticate')")
        b.wait_not_present(".pf-c-modal-box:contains('Switch to administrative access')")
        b.check_superuser_indicator("Administrative access")
        b.enter_page("/playground/test", host="10.111.113.2")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'result: ')
        self.assertIn('result: uid=0', b.text(".super-channel span"))

        self.allow_hostkey_messages()


@skipDistroPackage()
class TestSuperuserOldDashboard(MachineCase):
    provision = {
        "machine1": {"address": "10.111.113.1/20", "image": "centos-7"},
        "machine2": {"address": "10.111.113.2/20"},
    }

    def test(self):
        b = self.browser
        m = self.machines["machine1"]

        m.execute("firewall-cmd --add-service cockpit")
        m.start_cockpit()

        # Log into m1 and add m2
        b.open("/")
        b.wait_visible("#login")
        b.set_val("#login-user-input", "admin")
        b.set_val("#login-password-input", "foobar")
        b.set_checked('#authorized-input', True)
        b.click('#login-button')
        b.expect_load()
        b.wait_visible('#content')
        b.wait_visible('#content')

        b.go("/@10.111.113.2")
        b.wait_visible("#machine-troubleshoot")
        b.click('#machine-troubleshoot')
        b.wait_popup('troubleshoot-dialog')
        b.click('#troubleshoot-dialog button:contains("Add")')
        b.wait_in_text('#troubleshoot-dialog', "Fingerprint")
        b.click('#troubleshoot-dialog button:contains("Connect")')
        b.wait_popdown('troubleshoot-dialog')

        # There should be a superuser button in the Overview of machine2

        b.enter_page("/system", host="10.111.113.2")
        b.click(".ct-overview-header-actions button:contains('Administrative access')")
        b.click(".pf-c-modal-box:contains('Switch to limited access') button:contains('Limit access')")
        b.wait_not_present(".pf-c-modal-box:contains('Switch to limited access')")
        b.wait_visible(".ct-overview-header-actions button:contains('Limited access')")
        b.go("/@10.111.113.2/playground/test")
        b.enter_page("/playground/test", host="10.111.113.2")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'access-denied')

        b.go("/@10.111.113.2")
        b.enter_page("/system", host="10.111.113.2")
        b.click(".ct-overview-header-actions button:contains('Limited access')")
        b.wait_in_text(".pf-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
        b.set_input_text(".pf-c-modal-box:contains('Switch to administrative access') input", "foobar")
        b.click(".pf-c-modal-box button:contains('Authenticate')")
        b.wait_not_present(".pf-c-modal-box:contains('Switch to administrative access')")
        b.wait_visible(".ct-overview-header-actions button:contains('Administrative access')")
        b.go("/@10.111.113.2/playground/test")
        b.enter_page("/playground/test", host="10.111.113.2")
        b.click(".super-channel button")
        b.wait_in_text(".super-channel span", 'result: ')
        self.assertIn('result: uid=0', b.text(".super-channel span"))

        allow_old_cockpit_ws_messsages(self)
        self.allow_hostkey_messages()


@skipDistroPackage()
class TestSuperuserDashboardOldMachine(MachineCase):
    provision = {
        "machine1": {"address": "10.111.113.1/20"},
        "machine2": {"address": "10.111.113.2/20", "image": "centos-7"},
    }

    def test(self):
        b = self.browser

        self.login_and_go()
        b.go("/@10.111.113.2")
        b.wait_visible("#machine-troubleshoot")
        b.click('#machine-troubleshoot')
        b.wait_visible('#hosts_setup_server_dialog')
        b.click('#hosts_setup_server_dialog button:contains("Add")')
        b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")
        b.click('#hosts_setup_server_dialog button:contains("Accept key and connect")')
        b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")
        b.set_input_text("#login-custom-password", "foobar")
        b.click('#hosts_setup_server_dialog button:contains("Log in")')
        b.wait_not_present('#hosts_setup_server_dialog')

        # Since user and password are the same on machine2, we should
        # have gotten admin rights.

        b.enter_page("/system", host="10.111.113.2")
        b.click("#shutdown-group > button:contains('Restart')")
        b.wait_popup("shutdown-dialog")
        b.click("#shutdown-dialog button:contains('Restart')")
        b.wait_popdown("shutdown-dialog")

        self.allow_hostkey_messages()


if __name__ == '__main__':
    test_main()
