#!/usr/bin/python3

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


FP_MD5 = "93:40:9e:67:82:78:a8:99:89:39:d5:ba:e0:50:70:e1"
FP_SHA256 = "SHA256:SRvBhCmkCEVnJ6ascVH0AoVEbS3nPbowZkNevJnXtgw"
KEY = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDG4iipTovcMg0xn+089QLNKVGpP2Pgq2duxHgAXre2XgA3dZL+kooioGFwBQSEjbWssKy82hKIN/W82/lQtL6krf7JQWnT3LZwD5DPsvHFKhOLghbiFzSI0uEL4NFFcZOMo5tGLrM5LsZsaIkkv5QkAE0tHIyeYinK6dQ2d8ZsfmgqxHDUQUWnz1T75X9fWQsUugSWI+8xAe0cfa4qZRz/IC+K7DEB3x4Ot5pl8FBuydJj/gb+Lwo2Vs27/d87W/0KHCqOHNwaVC8RBb1WcmXRDDetLGH1A9m5x7Ip/KU/cyvWWxw8S4VKZkTIcrGUhFYJDnjtE3Axz+D7agtps41t test-name"


@skipImage("Do not test BaseOS packages", "rhel-8-3-distropkg", "rhel-8-4-distropkg")
class TestKeys(MachineCase):

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

        # Create a user without any role
        m.execute("useradd user -s /bin/bash -m -c 'User' || true")
        m.execute("echo user:foobar | chpasswd")

        m.start_cockpit()

        def login(user, password, user_name):
            b.open("/system")
            b.wait_visible("#login")
            b.set_val("#login-user-input", user)
            b.set_val("#login-password-input", password)
            b.click('#login-button')
            b.expect_load()
            b.enter_page("/system")

            b.go("/users#/" + user)
            b.enter_page("/users")
            b.wait_text("#account-user-name", user)

        def add_key(key, fp_md5, fp_sh256, comment):
            b.click('#authorized-key-add')
            b.wait_visible("#add-authorized-key-dialog")
            b.wait_val("#authorized-keys-text", "")
            b.set_input_text("#authorized-keys-text", key)
            b.click("#add-authorized-key-dialog button.apply")
            b.wait_not_present("#add-authorized-key-dialog")

            b.wait_in_text("#account-authorized-keys-list", comment)

            b.wait_not_in_text("#account-authorized-keys-list", "no authorized public keys")
            text = b.text("#account-authorized-keys-list")
            if ("SHA256" in text):
                self.assertIn(fp_sh256, text)
            else:
                self.assertIn(fp_md5, text)

        # no keys
        login("user", "foobar", "User")
        b.wait_in_text("#account-authorized-keys-list li.pf-c-data-list__item:first-child", "no authorized public keys")
        b.wait_js_func("ph_count_check", "#account-authorized-keys-list li.pf-c-data-list__item", 1)

        # add bad
        b.click('#authorized-key-add')
        b.wait_visible("#add-authorized-key-dialog")
        b.wait_val("#authorized-keys-text", "")
        b.set_input_text("#authorized-keys-text", "bad")
        b.click("#add-authorized-key-dialog button.apply")
        b.wait_in_text("#add-authorized-key-dialog", "The key you provided was not valid")
        b.click("#add-authorized-key-dialog button.cancel")

        # add good
        add_key(KEY, FP_MD5, FP_SHA256, "test-name")

        # Try see admin
        b.go("#/admin")
        b.wait_text("#account-user-name", "admin")

        # Not allowed, except on Ubuntu, where we can find out that ~/.ssh doesn't exist, which is shown as "no keys".
        if "ubuntu" not in m.image and "debian" not in m.image:
            b.wait_in_text("#account-authorized-keys-list li.pf-c-data-list__item:first-child",
                           "You do not have permission")
            b.wait_js_func("ph_count_check", "#account-authorized-keys-list li.pf-c-data-list__item", 1)

        b.logout()

        # delete whole ssh to start fresh
        m.execute("rm -rf /home/user/.ssh")
        self.assertNotIn(".ssh", m.execute("ls /home/user"))

        # Log in as admin
        login("admin", "foobar", "Administrator")
        b.go("#/user")
        b.wait_in_text("#account-authorized-keys-list li.pf-c-data-list__item:first-child", "no authorized public keys")
        b.wait_js_func("ph_count_check", "#account-authorized-keys-list li.pf-c-data-list__item", 1)

        # Adding keys sets permissions properly
        b.wait_text("#account-user-name", "user")
        add_key(KEY, FP_MD5, FP_SHA256, "test-name")
        perms = m.execute("getfacl -a /home/user/.ssh")
        self.assertIn("owner: user", perms)

        perms = m.execute("getfacl -a /home/user/.ssh/authorized_keys")
        self.assertIn("owner: user", perms)
        self.assertIn("user::rw-", perms)
        self.assertIn("group::---", perms)
        self.assertIn("other::---", perms)

        # Add invalid key directly
        m.execute("(echo '' && echo 'bad') >> /home/user/.ssh/authorized_keys")
        b.wait_in_text("#account-authorized-keys-list li.pf-c-data-list__item:last-child", "Invalid key")
        b.wait_js_func("ph_count_check", "#account-authorized-keys-list li.pf-c-data-list__item", 2)

        # Removing the key
        b.click("#account-authorized-keys-list li.pf-c-data-list__item:last-child button")
        b.wait_not_in_text("#account-authorized-keys-list li.pf-c-data-list__item:last-child", "Invalid key")
        b.wait_js_func("ph_count_check", "#account-authorized-keys-list li.pf-c-data-list__item", 1)
        data = m.execute("cat /home/user/.ssh/authorized_keys")
        self.assertEqual(data, KEY + '\n')
        b.logout()

        # User can still see their keys
        login("user", "foobar", "User")
        b.wait_in_text("#account-authorized-keys-list li.pf-c-data-list__item:first-child", "test-name")

        b.click("#account-authorized-keys-list li.pf-c-data-list__item:first-child button")
        b.wait_in_text("#account-authorized-keys-list li.pf-c-data-list__item:first-child", "no authorized public keys")
        b.wait_js_func("ph_count_check", "#account-authorized-keys-list li.pf-c-data-list__item", 1)

        self.allow_restart_journal_messages()
        self.allow_journal_messages('authorized_keys is not a public key file.')
        self.allow_journal_messages('Missing callback called fullpath = /home/user/.ssh/authorized_keys')
        self.allow_journal_messages('')

    # Possible workaround - ssh as `admin` and just do `m.execute()`
    @skipBrowser("Firefox cannot do `cockpit.spawn`", "firefox")
    def testPrivateKeys(self):
        b = self.browser
        m = self.machine

        def list_keys():
            return b.eval_js("cockpit.spawn([ '/bin/sh', '-c', 'ssh-add -l || true' ])")

        def assertKeyState(selector, enabled):
            b.wait_visible("%s input[type=checkbox]%s" % (selector, enabled and ":checked" or ":not(:checked)"))

        def toggleKeyState(selector, on_area=False):
            b.click(selector + " .pf-c-switch__input")

        # Operating systems where auto loading doesn't work
        auto_load = not m.ostree_image

        # Put all the keys in place
        m.execute("mkdir -p /home/admin/.ssh")
        m.upload([
            "verify/files/ssh/id_rsa",
            "verify/files/ssh/id_dsa",
            "verify/files/ssh/id_rsa.pub",
            "verify/files/ssh/id_dsa.pub"
        ], "/home/admin/.ssh/")
        m.execute("chmod 600 /home/admin/.ssh/*")
        m.execute("chown -R admin:admin /home/admin/.ssh")

        # Determine OpenSSH version
        ver = m.execute("ssh -V 2>&1 |  grep -Eo 'OpenSSH_[[:digit:]]+.[[:digit:]]+'")
        if ver:
            openssh_version = [int(x) for x in ver.split("_")[1].split(".")]
        else:
            openssh_version = [0]

        self.login_and_go()

        id_rsa = "2048 SHA256:SRvBhCmkCEVnJ6ascVH0AoVEbS3nPbowZkNevJnXtgw"
        old_rsa = "2048 93:40:9e:67:82:78:a8:99:89:39:d5:ba:e0:50:70:e1"
        id_dsa = "1024 SHA256:x6S6fxMuEyqhpwNRAIK7ms6bZDY6xK9wzdDr2kCaWVY"
        old_dsa = "1024 d4:55:41:20:f1:0a:e0:52:15:fc:fc:f0:63:22:1f:76"

        keys = list_keys()
        if auto_load:
            if "SHA256" in keys:
                self.assertIn(id_rsa, keys)
            else:
                self.assertIn(old_rsa, keys)
        self.assertNotIn(id_dsa, keys)
        self.assertNotIn(old_dsa, keys)

        b.click("#navbar-dropdown")
        b.click("#credentials-item")

        # Check the key display
        b.wait_visible("tbody[data-name=id_rsa]")

        # Automatically loaded
        if auto_load:
            assertKeyState("tbody[data-name=id_rsa]", True)
        b.click("tbody[data-name=id_rsa] tr.listing-ct-item")
        assert b.is_visible("tbody[data-name=id_rsa] .listing-ct-body")
        assert b.is_visible("tbody[data-name=id_rsa] ul")
        assert b.is_present("tbody[data-name=id_rsa] li.active:contains('Details')")
        self.assertEqual(b.text("tbody[data-name=id_rsa] .credential-comment"), "test@test")
        self.assertEqual(b.text("tbody[data-name=id_rsa] .credential-type"), "RSA")
        text = b.text("tbody[data-name=id_rsa] .credential-fingerprint")
        if "SHA256" in text:
            self.assertEqual(text, id_rsa[5:])
        else:
            self.assertEqual(text, old_rsa[5:])

        b.click("tbody[data-name=id_rsa] li:contains('Public key') a")
        assert b.is_visible("tbody[data-name=id_rsa] textarea")
        self.assertEqual(b.text("tbody[data-name=id_rsa] textarea"),
                         "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDG4iipTovcMg0xn+089QLNKVGpP" +
                         "2Pgq2duxHgAXre2XgA3dZL+kooioGFwBQSEjbWssKy82hKIN/W82/lQtL6krf7JQW" +
                         "nT3LZwD5DPsvHFKhOLghbiFzSI0uEL4NFFcZOMo5tGLrM5LsZsaIkkv5QkAE0tHIy" +
                         "eYinK6dQ2d8ZsfmgqxHDUQUWnz1T75X9fWQsUugSWI+8xAe0cfa4qZRz/IC+K7DEB" +
                         "3x4Ot5pl8FBuydJj/gb+Lwo2Vs27/d87W/0KHCqOHNwaVC8RBb1WcmXRDDetLGH1A" +
                         "9m5x7Ip/KU/cyvWWxw8S4VKZkTIcrGUhFYJDnjtE3Axz+D7agtps41t test@test\n")
        b.click("tbody[data-name=id_rsa] tr.listing-ct-item")
        assert not b.is_visible("tbody[data-name=id_rsa] .listing-ct-body")

        # Load the id_dsa key
        assertKeyState("tbody[data-name=id_dsa]", False)
        toggleKeyState("tbody[data-name=id_dsa]", True)
        assert b.is_present("tbody[data-name=id_dsa] .credential-unlock")
        b.set_val("tbody[data-name=id_dsa] .credential-password", "badbad")
        b.click("tbody[data-name=id_dsa] .credential-unlock button")
        b.wait_visible("tbody[data-name=id_dsa][data-loaded=1]")
        assertKeyState("tbody[data-name=id_dsa]", True)

        # Both keys are now loaded
        keys = list_keys()
        if "SHA256" in keys:
            if auto_load:
                self.assertIn(id_rsa, keys)
            self.assertIn(id_dsa, keys)
        else:
            if auto_load:
                self.assertIn(old_rsa, keys)
            self.assertIn(old_dsa, keys)

        # Unload the RSA key
        toggleKeyState("tbody[data-name=id_rsa]")
        b.wait_visible("tbody[data-name=id_rsa][data-loaded=0]")

        # Only DSA keys now loaded
        keys = list_keys()
        if "SHA256" in keys:
            self.assertIn(id_dsa, keys)
        else:
            self.assertIn(old_dsa, keys)
        self.assertNotIn(id_rsa, keys)
        self.assertNotIn(old_rsa, keys)

        # Change password of DSA key
        b.click("tbody[data-name=id_dsa] li:contains('Password') a")
        b.set_val("tbody[data-name=id_dsa] .credential-old", "badbad")
        b.set_val("tbody[data-name=id_dsa] .credential-new", "foobar")
        b.set_val("tbody[data-name=id_dsa] .credential-two", "foobar")
        b.click("tbody[data-name=id_dsa] .credential-change")
        b.wait_visible("tbody[data-name=id_dsa] li.active:contains('Details')")

        # Log off and log back in, and we should have both loaded automatically
        if auto_load:
            b.logout()
            b.login_and_go()
            keys = list_keys()
            if "SHA256" in keys:
                self.assertIn(id_rsa, keys)
                self.assertIn(id_dsa, keys)
            else:
                self.assertIn(old_rsa, keys)
                self.assertIn(old_dsa, keys)

            b.click("#navbar-dropdown")
            b.click("#credentials-item")
            b.wait_popup("credentials-dialog")

        # Add bad keys
        # generate a new key
        m.execute("ssh-keygen -t rsa -N '' -f /tmp/new.rsa")
        m.execute("chown admin:admin /tmp/new.rsa")
        new_pk = m.execute("cat /tmp/new.rsa.pub").strip().split()[0]
        m.execute("rm /tmp/new.rsa.pub")

        b.wait_visible("tbody[data-name=id_rsa]")

        b.wait_visible("#credential-keys")
        b.wait_visible("#credential-keys a")
        b.wait_not_visible("#ssh-file-add")
        b.click("#credential-keys a")
        b.wait_visible("tr.load-custom-key")
        b.wait_visible("#ssh-file-add")
        b.wait_visible("#ssh-file-container")
        b.set_val("#ssh-file-container input[type=text]", "/bad")
        b.click("#ssh-file-add")
        b.wait_text("tr.load-custom-key div.dialog-error", "Not a valid private key")

        b.set_val("#ssh-file-container input[type=text]", "/tmp/new.rsa")
        b.click("#ssh-file-add")
        b.wait_not_visible("tr.load-custom-key")
        # OpenSSH 7.8 and up has a new default key format where
        # keys are marked as "agent_only", thereby limiting functionality

        if openssh_version >= [7, 8]:
            new_key_tbody = "tbody:contains('root@')"
        else:
            new_key_tbody = "tbody[data-name='/tmp/new.rsa']"
        b.click(new_key_tbody)
        b.click(new_key_tbody + " tr.listing-ct-head")

        assert b.is_visible(new_key_tbody + " .listing-ct-body")
        assert b.is_visible(new_key_tbody + " ul")
        assert b.is_present(new_key_tbody + " li.active:contains('Details')")
        self.assertEqual(b.text(new_key_tbody + " .credential-type"), "RSA")
        b.wait_text_not(new_key_tbody + " .credential-fingerprint", "")
        b.click(new_key_tbody + " li:contains('Public key') a")
        assert b.is_visible(new_key_tbody + " textarea")
        self.assertTrue(b.text(new_key_tbody + " textarea").startswith(new_pk))

        # OpenSSH 7.8 and up has a new default key format where
        # keys are marked as "agent_only", thereby limiting functionality
        if openssh_version >= [7, 8]:
            # "agent_only" keys cannot be turned off, or have their passwords changed
            assertKeyState(new_key_tbody, True)
        else:
            # Change password of key
            b.click(new_key_tbody + " li:contains('Password') a")
            b.set_val(new_key_tbody + " .credential-old", "")
            b.set_val(new_key_tbody + " .credential-new", "foobar")
            b.set_val(new_key_tbody + " .credential-two", "foobar")
            b.click(new_key_tbody + " .credential-change")
            b.wait_visible(new_key_tbody + " li.active:contains('Details')")

            # Turn key off, it goes away
            toggleKeyState(new_key_tbody)
            b.wait_not_present(new_key_tbody)

            # Key now has password that needs to be entered to add
            b.wait_not_visible("tr.load-custom-key")
            b.click("#credential-keys a")
            b.wait_visible("tr.load-custom-key")
            b.wait_visible("#ssh-file-add")
            b.set_val("#ssh-file-container input[type=text]", "/bad")
            b.click("#ssh-file-add")

            b.set_val("#ssh-file-container input[type=text]", "/tmp/new.rsa")
            b.click("#ssh-file-add")
            b.wait_not_visible("tr.load-custom-key")
            b.wait_visible("tbody.ssh-add-key-body[data-name='/tmp/new.rsa']")

            assert b.is_present(new_key_tbody + " .credential-unlock")
            b.set_val(new_key_tbody + " .credential-password", "badbad")
            b.click(new_key_tbody + " .credential-unlock button")

            b.wait_visible(new_key_tbody + " .credential-alert")
            b.wait_in_text(new_key_tbody + " .credential-alert", "Password")
            b.set_val(new_key_tbody + " .credential-password", "foobar")
            b.click(new_key_tbody + " .credential-unlock button")

            b.wait_visible(new_key_tbody + "[data-loaded=1]")
            b.wait_not_present("tbody.ssh-add-key-body[data-name='/tmp/new.rsa']")



if __name__ == '__main__':
    test_main()
