356 lines
15 KiB
Python
Executable File
356 lines
15 KiB
Python
Executable File
#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
|
|
|
|
# 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/>.
|
|
|
|
from testlib import MachineCase, nondestructive, skipBrowser, skipDistroPackage, test_main
|
|
|
|
FP_SHA256 = "SHA256:iyVAl4Z8riL9Jg4fV9Wv/6cbqebdDtsBEMkojNLLYX8"
|
|
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEAkRTvQCSEZNPXpA5bP82ilQn3TMeQ6z2NO3O0UwY9z test-name"
|
|
|
|
|
|
@skipDistroPackage()
|
|
@nondestructive
|
|
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")
|
|
m.execute("echo user:foobar | chpasswd")
|
|
|
|
def login(user):
|
|
self.login_and_go("/users#/" + user, user=user, superuser=False)
|
|
b.wait_text("#account-user-name", user)
|
|
|
|
def add_key(key, 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")
|
|
self.assertIn(fp_sh256, text)
|
|
|
|
# no keys
|
|
login("user")
|
|
b.wait_in_text("#account-authorized-keys", "no authorized public keys")
|
|
|
|
# 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_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", "You do not have permission")
|
|
|
|
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")
|
|
b.go("#/user")
|
|
|
|
if "ubuntu" not in m.image and "debian" not in m.image:
|
|
b.wait_in_text("#account-authorized-keys",
|
|
"You do not have permission to view the authorized public keys for this account.")
|
|
|
|
b.become_superuser()
|
|
|
|
b.wait_in_text("#account-authorized-keys", "no authorized public keys")
|
|
|
|
def check_perms():
|
|
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)
|
|
|
|
# Adding keys sets permissions properly
|
|
b.wait_text("#account-user-name", "user")
|
|
add_key(KEY, FP_SHA256, "test-name")
|
|
check_perms()
|
|
|
|
b.wait_js_func("ph_count_check", "#account-authorized-keys-list tr", 1)
|
|
|
|
# Add invalid key directly
|
|
m.write("/home/user/.ssh/authorized_keys", "\nbad\n", append=True)
|
|
b.wait_in_text("#account-authorized-keys-list tr:last-child", "Invalid key")
|
|
b.wait_js_func("ph_count_check", "#account-authorized-keys-list tr", 2)
|
|
|
|
# Removing the key
|
|
b.click("#account-authorized-keys-list tr:last-child button")
|
|
b.wait_not_in_text("#account-authorized-keys", "Invalid key")
|
|
b.wait_js_func("ph_count_check", "#account-authorized-keys-list tr", 1)
|
|
data = m.execute("cat /home/user/.ssh/authorized_keys")
|
|
self.assertEqual(data, KEY + "\n\n")
|
|
# Permissions are still ok
|
|
check_perms()
|
|
b.logout()
|
|
|
|
# User can still see their keys
|
|
login("user")
|
|
b.wait_in_text("#account-authorized-keys-list tr:first-child", "test-name")
|
|
|
|
b.click("#account-authorized-keys-list tr:first-child button")
|
|
b.wait_in_text("#account-authorized-keys", "no authorized public keys")
|
|
|
|
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 toggleExpandedStateKey(identifier):
|
|
b.click(f"tr[data-name='{identifier}'] .pf-v5-c-table__toggle > button")
|
|
|
|
def waitKeyPresent(identifier, present):
|
|
if present:
|
|
b.wait_visible(f"tr[data-name='{identifier}']")
|
|
else:
|
|
b.wait_not_present(f"tr[data-name='{identifier}']")
|
|
|
|
def waitKeyLoaded(identifier, enabled):
|
|
b.wait_visible(f"tr[data-name='{identifier}'] input[type=checkbox]" + (":checked" if enabled else ":not(checked)"))
|
|
b.wait_visible(f"tr[data-name='{identifier}'][data-loaded={'true' if enabled else 'false'}]")
|
|
|
|
def toggleKeyState(identifier):
|
|
b.click(f"tr[data-name='{identifier}'] .pf-v5-c-switch__input")
|
|
|
|
def selectTab(identifier, title):
|
|
b.click(f"tr[data-name='{identifier}'] + tr li > button:contains('{title}')")
|
|
|
|
def waitTabActive(identifier, title):
|
|
b.wait_visible(f"tr[data-name='{identifier}'] + tr li.pf-m-current > button:contains('{title}')")
|
|
|
|
def waitKeyRowExpanded(identifier, expanded):
|
|
if expanded:
|
|
b.wait_visible(f"tr[data-name='{identifier}'] + tr .ct-listing-panel-body:not([hidden])")
|
|
else:
|
|
b.wait_not_visible(f"tr[data-name='{identifier}'] + tr")
|
|
|
|
def waitKeyDetail(identifier, dtype, value):
|
|
b.wait_in_text(f"tr[data-name='{identifier}'] + tr dt:contains({dtype}) + dd > div", value)
|
|
|
|
def getKeyDetail(identifier, dtype):
|
|
return b.text(f"tr[data-name='{identifier}'] + tr dt:contains({dtype}) + dd > div")
|
|
|
|
# Operating systems where auto loading doesn't work
|
|
auto_load = not m.ostree_image
|
|
|
|
if m.image == "arch":
|
|
self.write_file("/etc/pam.d/cockpit", """
|
|
auth optional pam_ssh_add.so
|
|
session optional pam_ssh_add.so
|
|
""", append=True)
|
|
|
|
self.restore_dir("/home/admin")
|
|
|
|
# Put all the keys in place
|
|
m.execute("mkdir -p /home/admin/.ssh")
|
|
m.upload([
|
|
"verify/files/ssh/id_rsa",
|
|
"verify/files/ssh/id_rsa.pub",
|
|
"verify/files/ssh/id_ed25519",
|
|
"verify/files/ssh/id_ed25519.pub"
|
|
], "/home/admin/.ssh/")
|
|
m.execute("chmod 600 /home/admin/.ssh/*")
|
|
m.execute("chown -R admin:admin /home/admin/.ssh")
|
|
|
|
self.login_and_go()
|
|
|
|
id_rsa = "2048 SHA256:SRvBhCmkCEVnJ6ascVH0AoVEbS3nPbowZkNevJnXtgw"
|
|
id_ed25519 = "256 SHA256:Wd028KYmG3OVLp7dBmdx0gMR7VcarJVIfaTtKqYCmak"
|
|
|
|
keys = list_keys()
|
|
if auto_load:
|
|
self.assertIn(id_rsa, keys)
|
|
self.assertNotIn(id_ed25519, keys)
|
|
|
|
b.open_session_menu()
|
|
b.click("#sshkeys")
|
|
|
|
# Automatically loaded
|
|
waitKeyLoaded('id_rsa', auto_load)
|
|
toggleExpandedStateKey('id_rsa')
|
|
waitKeyRowExpanded('id_rsa', True)
|
|
waitTabActive('id_rsa', 'Details')
|
|
waitKeyDetail('id_rsa', 'Comment', "test@test")
|
|
waitKeyDetail('id_rsa', 'Type', "RSA")
|
|
text = b.text("tr[data-name=id_rsa] + tr dt:contains(Fingerprint) + dd > div")
|
|
self.assertEqual(text, id_rsa[5:])
|
|
|
|
selectTab('id_rsa', 'Public key')
|
|
b.wait_val("tr[data-name=id_rsa] + tr .pf-v5-c-clipboard-copy input",
|
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDG4iipTovcMg0xn+089QLNKVGpP"
|
|
"2Pgq2duxHgAXre2XgA3dZL+kooioGFwBQSEjbWssKy82hKIN/W82/lQtL6krf7JQW"
|
|
"nT3LZwD5DPsvHFKhOLghbiFzSI0uEL4NFFcZOMo5tGLrM5LsZsaIkkv5QkAE0tHIy"
|
|
"eYinK6dQ2d8ZsfmgqxHDUQUWnz1T75X9fWQsUugSWI+8xAe0cfa4qZRz/IC+K7DEB"
|
|
"3x4Ot5pl8FBuydJj/gb+Lwo2Vs27/d87W/0KHCqOHNwaVC8RBb1WcmXRDDetLGH1A"
|
|
"9m5x7Ip/KU/cyvWWxw8S4VKZkTIcrGUhFYJDnjtE3Axz+D7agtps41t test@test")
|
|
toggleExpandedStateKey('id_rsa')
|
|
waitKeyRowExpanded('id_rsa', False)
|
|
|
|
# Load the id_ed25519 key
|
|
waitKeyLoaded('id_ed25519', False)
|
|
toggleKeyState('id_ed25519')
|
|
b.set_input_text("#id_ed25519-password", "locked")
|
|
b.click("#id_ed25519-unlock")
|
|
waitKeyLoaded('id_ed25519', True)
|
|
|
|
# Both keys are now loaded
|
|
keys = list_keys()
|
|
if auto_load:
|
|
self.assertIn(id_rsa, keys)
|
|
self.assertIn(id_ed25519, keys)
|
|
|
|
# Unload the RSA key
|
|
if auto_load:
|
|
toggleKeyState('id_rsa')
|
|
waitKeyLoaded('id_rsa', False)
|
|
|
|
# Only DSA keys now loaded
|
|
keys = list_keys()
|
|
self.assertIn(id_ed25519, keys)
|
|
self.assertNotIn(id_rsa, keys)
|
|
|
|
# Change password of DSA key
|
|
toggleExpandedStateKey('id_ed25519')
|
|
selectTab('id_ed25519', 'Password')
|
|
b.set_input_text("#id_ed25519-old-password", "locked")
|
|
b.set_input_text("#id_ed25519-new-password", "foobar")
|
|
b.set_input_text("#id_ed25519-confirm-password", "foobar")
|
|
b.assert_pixels("#credentials-modal", "ssh-keys-dialog")
|
|
|
|
b.click("#id_ed25519-change-password")
|
|
b.wait_visible('#credentials-modal .pf-v5-c-helper-text__item.pf-m-success')
|
|
|
|
# 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()
|
|
self.assertIn(id_rsa, keys)
|
|
self.assertIn(id_ed25519, keys)
|
|
|
|
b.open_session_menu()
|
|
b.click("#sshkeys")
|
|
b.wait_visible("#credentials-modal")
|
|
|
|
# Add bad keys
|
|
# generate a new key
|
|
m.execute("ssh-keygen -t rsa -N '' -f /tmp/new.rsa")
|
|
self.addCleanup(m.execute, "rm -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")
|
|
|
|
waitKeyPresent('id_rsa', True)
|
|
|
|
b.wait_visible("#credential-keys")
|
|
b.wait_not_present("#ssh-file-add")
|
|
b.click("#ssh-file-add-custom")
|
|
b.set_file_autocomplete_val("#ssh-file-add-key", "/bad")
|
|
b.click("#ssh-file-add")
|
|
b.wait_text("#credentials-modal .pf-m-error > .pf-v5-c-helper-text__item-text", "Not a valid private key")
|
|
|
|
b.click("#credentials-modal .pf-v5-c-select__toggle-typeahead")
|
|
b.set_input_text("#credentials-modal .pf-v5-c-select__toggle-typeahead", "/var/test/")
|
|
b.wait_in_text("#credentials-modal .pf-v5-c-select__menu-item.pf-m-disabled", "No such file or directory")
|
|
b.focus("#credentials-modal .pf-v5-c-select__toggle-typeahead")
|
|
b.key_press("\b\b\b\b\b")
|
|
b.wait_visible("#credentials-modal .pf-v5-c-select__menu-item.directory:contains('/var/lib/')")
|
|
b.click("#credentials-modal .pf-v5-c-select__toggle-clear")
|
|
b.wait_val("#credentials-modal .pf-v5-c-select__toggle-typeahead", "")
|
|
|
|
b.set_file_autocomplete_val("#ssh-file-add-key", "/tmp/new.rsa")
|
|
b.click("#ssh-file-add")
|
|
b.wait_not_present("#ssh-file-add")
|
|
# OpenSSH 7.8 and up has a new default key format where
|
|
# keys are marked as "agent_only", thereby limiting functionality
|
|
keys = list_keys()
|
|
keys_length = 3 if auto_load else 2
|
|
# Keys are like: 256 SHA256:x6S6fxMuEyqhpwNRAIK7ms6bZDY6xK9wzdDr2kCaWVY id_ed25519 (ED25519)
|
|
# We need the id_ed25519 part, (name or comment)
|
|
new_key = keys.splitlines()[keys_length - 1].split(' ')[2]
|
|
toggleKeyState(new_key)
|
|
|
|
toggleExpandedStateKey(new_key)
|
|
waitKeyRowExpanded(new_key, True)
|
|
waitTabActive(new_key, 'Details')
|
|
waitKeyDetail(new_key, 'Type', 'RSA')
|
|
self.assertNotEqual(getKeyDetail(new_key, 'Fingerprint'), "")
|
|
selectTab(new_key, 'Public key')
|
|
self.assertTrue(b.val(f"tr[data-name='{new_key}'] + tr .pf-v5-c-clipboard-copy input").startswith(new_pk))
|
|
|
|
# OpenSSH 7.8 and up has a new default key format where
|
|
# keys are marked as "agent_only", thereby limiting functionality
|
|
# "agent_only" keys cannot be turned off, or have their passwords changed
|
|
waitKeyLoaded(new_key, True)
|
|
|
|
# Test adding key with passphrase
|
|
m.execute("ssh-keygen -t rsa -N 'foobar' -f /tmp/new_with_passphrase.rsa")
|
|
self.addCleanup(m.execute, "rm -f /tmp/new_with_passphrase.rsa*")
|
|
m.execute("chown admin:admin /tmp/new_with_passphrase.rsa")
|
|
m.execute("rm /tmp/new_with_passphrase.rsa.pub")
|
|
|
|
b.wait_not_present("#ssh-file-add")
|
|
b.click("#ssh-file-add-custom")
|
|
b.set_file_autocomplete_val("#ssh-file-add-key", "/tmp/new_with_passphrase.rsa")
|
|
b.click("#ssh-file-add")
|
|
b.wait_visible("h1:contains('Unlock key /tmp/new_with_passphrase.rsa')")
|
|
b.set_input_text("#\\/tmp\\/new_with_passphrase\\.rsa-password", "foobar")
|
|
b.click("button:contains('Unlock')")
|
|
b.wait_not_present("h1:contains('Unlock key /tmp/new_with_passphrase.rsa')")
|
|
b.wait_not_present("#ssh-file-add")
|
|
b.wait_js_func("ph_count_check", "#credential-keys tbody", 4)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
test_main()
|