1260 lines
55 KiB
Python
Executable File
1260 lines
55 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) 2013 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 datetime
|
|
import os
|
|
|
|
from testlib import MachineCase, nondestructive, skipDistroPackage, skipOstree, test_main, wait
|
|
|
|
good_password = "tqymuVh.ZfZnP§9Wr=LM3JyG5yx"
|
|
|
|
|
|
useradd_defaults = {}
|
|
|
|
|
|
def getUserAddDetails(machine):
|
|
global useradd_defaults
|
|
|
|
if not useradd_defaults: # dictionary is empty
|
|
useradd_defaults_str = machine.execute("useradd -D")
|
|
lines = useradd_defaults_str.split("\n")
|
|
|
|
for line in lines:
|
|
if line:
|
|
key, value = line.split("=")
|
|
useradd_defaults[key] = value
|
|
|
|
return useradd_defaults
|
|
|
|
|
|
def performUserAction(browser, user, action):
|
|
browser.wait_visible(f"#accounts-list tbody tr:contains({user}) .pf-v5-c-dropdown button")
|
|
browser.click(f"#accounts-list tbody tr:contains({user}) .pf-v5-c-dropdown button")
|
|
browser.wait_visible(f"#accounts-list tbody tr:contains({user}) .pf-v5-c-dropdown button[aria-expanded=true]")
|
|
browser.click(f".pf-v5-c-dropdown__menu-item:contains({action})")
|
|
|
|
|
|
def createUser(
|
|
browser,
|
|
machine,
|
|
user_name,
|
|
real_name,
|
|
locked,
|
|
force_password_change,
|
|
password=None,
|
|
custom_home_dir=None,
|
|
default_shell=None,
|
|
custom_shell=None,
|
|
uid=None,
|
|
expected_uid=None,
|
|
verify_created=True,
|
|
run_assert_pixels=False,
|
|
):
|
|
if default_shell is None:
|
|
default_shell = getUserAddDetails(machine)["SHELL"] or "/bin/bash"
|
|
browser.wait_visible('#accounts-create')
|
|
browser.click('#accounts-create')
|
|
browser.wait_visible('#accounts-create-dialog')
|
|
if run_assert_pixels:
|
|
browser.assert_pixels("#accounts-create-dialog", "accounts-create-dialog")
|
|
|
|
browser.set_input_text('#accounts-create-user-name', user_name)
|
|
browser.set_input_text('#accounts-create-real-name', real_name)
|
|
if password:
|
|
browser.set_input_text('#accounts-create-password-pw1', password)
|
|
browser.set_input_text('#accounts-create-password-pw2', password)
|
|
|
|
if locked:
|
|
browser.click('#accounts-create-locked')
|
|
browser.wait_visible('#accounts-create-locked:checked')
|
|
else:
|
|
browser.wait_visible('#account-use-password:checked')
|
|
browser.set_checked('#accounts-create-force-password-change', force_password_change)
|
|
browser.wait_visible('#accounts-create-locked:not(:checked)')
|
|
|
|
if expected_uid is not None:
|
|
browser.wait_visible(f'#accounts-create-user-uid[value="{expected_uid}"]')
|
|
if uid is not None:
|
|
browser.set_input_text('#accounts-create-user-uid', uid)
|
|
if custom_home_dir is not None:
|
|
browser.set_input_text('#accounts-create-user-home-dir', custom_home_dir)
|
|
else:
|
|
default_home_dir = getUserAddDetails(machine)["HOME"]
|
|
expected_home_dir = default_home_dir + "/" + user_name
|
|
browser.wait_visible(f"#accounts-create-user-home-dir[value='{expected_home_dir}']")
|
|
|
|
if custom_shell:
|
|
browser.select_from_dropdown("#accounts-create-user-shell", custom_shell)
|
|
else:
|
|
browser.wait_visible(f"#accounts-create-user-shell[data-selected='{default_shell}']")
|
|
|
|
browser.click('#accounts-create-dialog button.apply')
|
|
|
|
if verify_created:
|
|
browser.wait_not_present('#accounts-create-dialog')
|
|
browser.wait_in_text('#accounts-list', real_name)
|
|
|
|
|
|
@nondestructive
|
|
@skipDistroPackage()
|
|
class TestAccounts(MachineCase):
|
|
|
|
def testBasic(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
self.login_and_go("/users")
|
|
|
|
# Add a user externally
|
|
m.execute("useradd anton")
|
|
m.execute("echo anton:foobar | chpasswd")
|
|
with b.wait_timeout(30):
|
|
b.wait_in_text('#accounts-list', "anton")
|
|
|
|
# FIXME: rtl test was flaky, debug it and remove the skip
|
|
b.assert_pixels("#users-page", "users-page", ignore=["td[data-label='Last active']", "button:contains('more...')"], skip_layouts=["rtl"])
|
|
|
|
# There is only one badge and it is for admin
|
|
b.wait_text('#current-account-badge', 'Your account')
|
|
b.wait_js_cond('document.querySelector("#current-account-badge").previousSibling.getAttribute("href") === "#/admin"')
|
|
|
|
# The current account is the first in the list
|
|
b.wait_visible("#accounts-list > tbody :first-child #current-account-badge")
|
|
|
|
# Set a real name
|
|
b.go("#/anton")
|
|
b.assert_pixels("#users-page", "user-detail-page")
|
|
b.wait_text("#account-user-name", "anton")
|
|
b.wait_text("#account-title", "anton")
|
|
b.wait_not_attr("#account-delete", "disabled", "disabled")
|
|
b.set_input_text('#account-real-name', "") # Check that we can delete the name before setting it up
|
|
b.set_input_text('#account-real-name', "Anton Arbitrary")
|
|
b.wait_visible('#account-real-name:not([disabled])')
|
|
b.wait_text("#account-title", "Anton Arbitrary")
|
|
b.wait_text("#account-home-dir", os.path.join(getUserAddDetails(m)["HOME"] + "/anton"))
|
|
b.wait_text("#account-shell", getUserAddDetails(m)["SHELL"])
|
|
self.assertIn(":Anton Arbitrary:", m.execute("grep anton /etc/passwd"))
|
|
|
|
# Add some other GECOS fields
|
|
b.set_input_text('#account-real-name', "Anton Arbitrary,1,123")
|
|
b.wait_visible('#account-real-name:not([disabled])')
|
|
self.assertIn(":Anton Arbitrary,1,123:", m.execute("grep anton /etc/passwd"))
|
|
# Table title only shows real name, no other GECOS fields
|
|
b.wait_text("#account-title", "Anton Arbitrary")
|
|
# On the overview page it also shows only real name
|
|
b.go("/users")
|
|
b.wait_text('#accounts-list td[data-label="Full name"]:contains("Anton")', "Anton Arbitrary")
|
|
b.go("/users/#anton")
|
|
|
|
# Delete it
|
|
b.click('#account-delete')
|
|
b.wait_visible('#account-confirm-delete-dialog')
|
|
b.click('#account-confirm-delete-dialog button.apply')
|
|
b.wait_not_present('#account-confirm-delete-dialog')
|
|
b.wait_visible("#accounts")
|
|
b.wait_not_in_text('#accounts-list', "Anton Arbitrary")
|
|
|
|
# Attempt a real name with a colon
|
|
b.click('#accounts-create')
|
|
b.wait_visible('#accounts-create-dialog')
|
|
b.set_input_text('#accounts-create-real-name', "Col:n Colon") # This should fail
|
|
b.set_input_text('#accounts-create-password-pw1', good_password)
|
|
b.set_input_text('#accounts-create-password-pw2', good_password)
|
|
b.click('#accounts-create-dialog button.apply')
|
|
b.wait_in_text("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-error", "The full name must not contain colons.")
|
|
b.click('#accounts-create-dialog button.cancel')
|
|
b.wait_visible("#accounts")
|
|
|
|
# Check root user
|
|
b.go("#/root")
|
|
b.wait_text("#account-user-name", "root")
|
|
# some operations are not allowed for root user
|
|
b.wait_visible("#account-delete[disabled]")
|
|
b.wait_visible("#account-real-name[disabled]")
|
|
b.wait_visible("#account-logout[disabled]")
|
|
b.wait_visible("#account-locked:not(:checked)")
|
|
# check home directory and shell for root
|
|
b.wait_text("#account-home-dir", "/root")
|
|
b.wait_text("#account-shell", "/bin/bash")
|
|
# root account should not be locked by default on our images
|
|
self.assertIn(m.execute("passwd -S root").split()[1], ["P", "PS"])
|
|
# now lock account
|
|
b.set_checked("#account-locked", True)
|
|
b.wait(lambda: m.execute("passwd -S root").split()[1] in ["L", "LK"])
|
|
|
|
# go back to accounts overview, check pf-v5-c-breadcrumb
|
|
b.click("#account .pf-v5-c-breadcrumb a")
|
|
b.wait_visible("#accounts-create")
|
|
|
|
# Create a user from the UI
|
|
self.sed_file('s@^SHELL=.*$@SHELL=/bin/true@', '/etc/default/useradd')
|
|
b.click('#accounts-create')
|
|
b.wait_visible('#accounts-create-dialog')
|
|
b.set_input_text('#accounts-create-user-name', "Berta")
|
|
b.set_input_text('#accounts-create-real-name', "Berta Bestimmt")
|
|
b.set_input_text('#accounts-create-password-pw1', "foo")
|
|
b.wait_visible("#accounts-create-password-meter.danger")
|
|
b.set_input_text('#accounts-create-password-pw1', good_password)
|
|
b.wait_visible("#accounts-create-password-meter.success")
|
|
|
|
# wrong password confirmation
|
|
b.set_input_text('#accounts-create-password-pw2', good_password + 'b')
|
|
b.click('#accounts-create-dialog button.apply')
|
|
b.wait_in_text("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-error", "The passwords do not match")
|
|
b.wait_not_present('#accounts-create-dialog button.pf-m-warning')
|
|
|
|
# too long password
|
|
long_password = "2a02-x!h4a" * 30
|
|
b.set_input_text('#accounts-create-password-pw1', long_password)
|
|
b.set_input_text('#accounts-create-password-pw2', long_password)
|
|
b.click('#accounts-create-dialog button.apply')
|
|
b.wait_in_text("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-warning", "Password is longer than 256 characters")
|
|
b.wait_not_present('#accounts-create-dialog button.pf-m-warning')
|
|
|
|
# changing input clears the error message
|
|
b.set_input_text('#accounts-create-password-pw1', "test")
|
|
b.set_input_text('#accounts-create-password-pw2', "test")
|
|
b.wait_not_present("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-warning")
|
|
|
|
# correct password confirmation
|
|
b.set_input_text('#accounts-create-password-pw1', good_password)
|
|
b.set_input_text('#accounts-create-password-pw2', good_password)
|
|
b.click('#accounts-create-dialog button.apply')
|
|
b.wait_not_present("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-warning")
|
|
b.wait_not_present('#accounts-create-dialog')
|
|
b.wait_in_text('#accounts-list', "Berta Bestimmt")
|
|
|
|
# Check home directory
|
|
home_dir = m.execute("getent passwd Berta | cut -f6 -d:").strip()
|
|
self.assertTrue(home_dir.endswith("/Berta"))
|
|
self.assertEqual(m.execute(f"stat -c '%U' {home_dir}").strip(), "Berta")
|
|
|
|
# Check that we set up shell configured in /etc/default/useradd
|
|
shell = m.execute("getent passwd Berta | cut -f7 -d:").strip()
|
|
self.assertEqual(shell, '/bin/true')
|
|
|
|
# Delete it externally
|
|
m.execute("userdel Berta")
|
|
b.wait_not_in_text('#accounts-list', "Berta Bestimmt")
|
|
|
|
b.logout()
|
|
b.login_and_go("/users")
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="robert",
|
|
real_name="Robert Robertson",
|
|
password=good_password,
|
|
locked=False,
|
|
force_password_change=True,
|
|
default_shell="/bin/true",
|
|
run_assert_pixels=True
|
|
)
|
|
|
|
# Test actions in kebab menu
|
|
# disable password
|
|
performUserAction(b, 'robert', 'Lock account')
|
|
b.click("#account-confirm-lock-dialog footer .pf-m-danger.apply")
|
|
# lock option is now disabled
|
|
b.click("#accounts-list tbody tr:contains(robert) .pf-v5-c-dropdown button")
|
|
b.wait_in_text(".pf-v5-c-dropdown__menu li:contains('Lock account') .pf-m-disabled", 'Lock account')
|
|
b.click("#accounts-list tbody tr:contains(robert) .pf-v5-c-dropdown button")
|
|
# change is visible on details page
|
|
performUserAction(b, 'robert', 'Edit user')
|
|
b.wait_in_text('#account-title', 'Robert Robertson')
|
|
b.wait_visible('#account-locked:checked')
|
|
b.click("#account-locked")
|
|
b.go("/users")
|
|
|
|
# In fedora-core userdel for this user consistently fails
|
|
# userdel: user robert is currently used by process *
|
|
if not m.ostree_image:
|
|
performUserAction(b, 'robert', 'Delete account')
|
|
b.click("#account-confirm-delete-dialog footer button.pf-m-danger.apply")
|
|
b.wait_not_in_text('#accounts-list', "Robert Robertson")
|
|
|
|
self.allow_journal_messages("Password quality check failed:")
|
|
self.allow_journal_messages("The password is a palindrome")
|
|
self.allow_journal_messages("passwd: user.*does not exist")
|
|
self.allow_journal_messages("passwd: Unknown user name '.*'.")
|
|
self.allow_journal_messages("lastlog: Unknown user or range: anton")
|
|
# when sed'ing, there is a short time when the config file does not exist
|
|
self.allow_journal_messages(".*libuser initialization error: .*/etc/default/useradd.*: No such file or directory")
|
|
|
|
def testFilterAndSort(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
self.login_and_go("/users")
|
|
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="robert",
|
|
real_name="Robert Robertson",
|
|
password=good_password,
|
|
locked=False,
|
|
force_password_change=True,
|
|
)
|
|
|
|
# Check Robert's groups
|
|
b.wait_not_present("#accounts-list tbody tr:contains(robert) td[data-label='Group'] .pf-v5-c-label:contains(users)")
|
|
if not m.ostree_image: # Users group does not exist in coreos image
|
|
m.execute("/usr/bin/gpasswd -a robert users")
|
|
b.wait_in_text("#accounts-list tbody tr:contains(robert) td[data-label='Group'] .pf-v5-c-label.pf-m-cyan:contains(users)", "users")
|
|
m.execute(f"/usr/bin/gpasswd -a robert {m.get_admin_group()}")
|
|
b.wait_in_text("#accounts-list tbody tr:contains(robert) td[data-label='Group'] .pf-v5-c-label.pf-m-gold", m.get_admin_group())
|
|
m.execute(f"/usr/bin/gpasswd -d robert {m.get_admin_group()}")
|
|
b.wait_not_present("#accounts-list tbody tr:contains(robert) td[data-label='Group'] .pf-v5-c-label.pf-m-gold")
|
|
|
|
# test filters
|
|
b.set_input_text("#accounts-filter input", "root")
|
|
b.wait_in_text("#accounts-list tbody tr:first-child td[data-label='Username']", "root")
|
|
b.set_input_text("#accounts-filter input", "rOBeRt")
|
|
b.wait_in_text("#accounts-list tbody tr:first-child td[data-label='Username']", "robert")
|
|
|
|
uid = "1000"
|
|
if "debian" in m.image or "ubuntu" in m.image:
|
|
uid = "1001"
|
|
b.set_input_text("#accounts-filter input", uid)
|
|
b.wait_in_text("#accounts-list tbody tr:first-child td[data-label='ID']", uid)
|
|
b.set_input_text("#accounts-filter input", "spooky")
|
|
b.wait_visible("#accounts div.pf-v5-c-empty-state")
|
|
|
|
b.inject_js("""
|
|
function getTextColumn(query_selector) {
|
|
const values = [];
|
|
document.querySelectorAll(query_selector).forEach(node => values.push(node.innerText));
|
|
return values;
|
|
}""")
|
|
|
|
def check_column_sort(query_selector, invert=False):
|
|
# current account is always in the first row
|
|
b.wait_in_text("#accounts-list tbody tr:first-child td[data-label='Username']", "admin")
|
|
values = b.eval_js(f"getTextColumn(\"{query_selector}\")")
|
|
for i in range(2, len(values)):
|
|
if values[i].isnumeric():
|
|
value_current = int(values[i])
|
|
value_prev = int(values[i - 1])
|
|
else:
|
|
value_current = values[i].lower()
|
|
value_prev = values[i - 1].lower()
|
|
|
|
if (invert):
|
|
b.wait(lambda: value_prev > value_current)
|
|
else:
|
|
b.wait(lambda: value_prev < value_current)
|
|
|
|
# robert should be in users group
|
|
if not m.ostree_image: # Users group does not exist in coreos image
|
|
b.set_input_text("#accounts-filter input", "users")
|
|
names = b.eval_js("getTextColumn(\"[data-label='Username'] a\")")
|
|
b.wait(lambda: "robert" in names)
|
|
|
|
# clear text filters
|
|
b.click("#accounts-filter button[aria-label='Reset']")
|
|
|
|
# check alphabetical order of Username
|
|
check_column_sort("[data-label='Username'] a")
|
|
b.wait_visible("#accounts-list > thead > tr > th:nth-child(1) > button")
|
|
b.click("#accounts-list > thead > tr > th:nth-child(1) > button")
|
|
check_column_sort("[data-label='Username'] a", invert=True)
|
|
|
|
# sort by full name
|
|
b.click("#accounts-list > thead > tr > th:nth-child(2) > button")
|
|
check_column_sort("[data-label='Full name']")
|
|
b.click("#accounts-list > thead > tr > th:nth-child(2) > button")
|
|
check_column_sort("[data-label='Full name']", invert=True)
|
|
|
|
# sort by ID
|
|
b.click("#accounts-list > thead > tr > th:nth-child(3) > button")
|
|
check_column_sort("[data-label='ID']", invert=True)
|
|
b.click("#accounts-list > thead > tr > th:nth-child(3) > button")
|
|
check_column_sort("[data-label='ID']")
|
|
|
|
def testUserPasswords(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
self.login_and_go("/users")
|
|
# Create a locked user with weak password
|
|
self.sed_file('s/^SHELL=.*$/SHELL=/', '/etc/default/useradd')
|
|
self.allow_journal_messages(".*required to change your password immediately.*")
|
|
self.allow_journal_messages(".*user account or password has expired.*")
|
|
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="js",
|
|
real_name="Jussi Senior",
|
|
locked=True,
|
|
force_password_change=False,
|
|
verify_created=False,
|
|
)
|
|
|
|
# Check Confirm password is not validated until it's at least the same length as the first password
|
|
# Once they are the same length, it should valitate the Confirm password after each keystroke
|
|
b.set_input_text('#accounts-create-password-pw1', "foobar")
|
|
b.set_input_text('#accounts-create-password-pw2', "bar")
|
|
b.wait_not_present("#accounts-create-password-pw2-helper")
|
|
b.set_input_text('#accounts-create-password-pw2', "foobarfoo")
|
|
b.wait_visible("#accounts-create-password-pw2-helper")
|
|
b.wait_in_text("#accounts-create-password-pw2-helper", "The passwords do not match")
|
|
b.set_input_text('#accounts-create-password-pw2', "bar")
|
|
b.wait_visible("#accounts-create-password-pw2-helper")
|
|
b.wait_in_text("#accounts-create-password-pw2-helper", "The passwords do not match")
|
|
b.set_input_text('#accounts-create-password-pw2', "foobar")
|
|
b.wait_not_present("#accounts-create-password-pw2-helper")
|
|
|
|
b.click('#accounts-create-dialog button.cancel')
|
|
b.wait_not_present('#account-set-password-dialog')
|
|
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="js",
|
|
real_name="Jussi Senior",
|
|
locked=True,
|
|
force_password_change=False,
|
|
verify_created=False,
|
|
)
|
|
|
|
# Check Confirm password is not validated until the form is submitted
|
|
# Once it is submitted, it should valitate the Confirm password after each keystroke
|
|
b.set_input_text('#accounts-create-password-pw1', "foobar")
|
|
b.set_input_text('#accounts-create-password-pw2', "bar")
|
|
b.wait_not_present("#accounts-create-password-pw2-helper")
|
|
b.click('#accounts-create-dialog button.apply')
|
|
b.wait_visible("#accounts-create-password-pw2-helper")
|
|
b.wait_in_text("#accounts-create-password-pw2-helper", "The passwords do not match")
|
|
b.set_input_text('#accounts-create-password-pw2', "barfoo")
|
|
b.wait_visible("#accounts-create-password-pw2-helper")
|
|
b.wait_in_text("#accounts-create-password-pw2-helper", "The passwords do not match")
|
|
b.set_input_text('#accounts-create-password-pw2', "foobarfoo")
|
|
b.wait_visible("#accounts-create-password-pw2-helper")
|
|
b.wait_in_text("#accounts-create-password-pw2-helper", "The passwords do not match")
|
|
b.set_input_text('#accounts-create-password-pw2', "foobar")
|
|
b.wait_not_present("#accounts-create-password-pw2-helper")
|
|
|
|
b.click('#accounts-create-dialog button.cancel')
|
|
b.wait_not_present('#account-set-password-dialog')
|
|
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="jussi",
|
|
real_name="Jussi Junior",
|
|
password="foo",
|
|
locked=True,
|
|
force_password_change=False,
|
|
verify_created=False,
|
|
)
|
|
|
|
# Password is weak, lets change it to another weak - this should still not accept
|
|
b.wait_in_text("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-warning", "Password quality check failed:")
|
|
b.wait_visible('#accounts-create-dialog button.pf-m-warning')
|
|
b.set_input_text('#accounts-create-password-pw1', "bar")
|
|
b.set_input_text('#accounts-create-password-pw2', "bar")
|
|
b.wait_not_present("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-warning")
|
|
b.wait_not_present('#accounts-create-dialog button.pf-m-warning')
|
|
b.click('#accounts-create-dialog button.apply')
|
|
|
|
# Password is weak, confirm button should be disabled after first click
|
|
b.wait_in_text("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-warning", "Password quality check failed:")
|
|
b.wait_visible("button.apply:disabled")
|
|
|
|
# Lets confirm the weak password now
|
|
b.click('#accounts-create-dialog button.pf-m-warning')
|
|
|
|
b.wait_not_present('#accounts-create-dialog')
|
|
b.wait_in_text('#accounts-list', "Jussi Junior")
|
|
|
|
def is_locked():
|
|
return m.execute("passwd -S jussi | cut -d' ' -f2").strip() in ["L", "LK"]
|
|
|
|
def is_admin():
|
|
return "jussi" in m.execute(f"getent group {m.get_admin_group()}")
|
|
|
|
admin_role_sel = '#account-groups-form-group'
|
|
b.wait(lambda: "jussi" in m.execute("grep jussi /etc/passwd"))
|
|
b.wait(lambda: not is_admin())
|
|
b.wait(is_locked)
|
|
|
|
# Check that by default we set up `/bin/bash`
|
|
shell = m.execute("getent passwd jussi | cut -f7 -d:").strip()
|
|
self.assertEqual(shell, '/bin/bash')
|
|
|
|
# Unlock it and make it an admin
|
|
b.go("#/jussi")
|
|
b.wait_text("#account-user-name", "jussi")
|
|
b.wait_visible("#account-locked:checked")
|
|
b.set_checked('#account-locked', False)
|
|
b.wait(lambda: not is_locked())
|
|
b.wait_not_present(admin_role_sel + f" .pf-v5-c-label:contains(:{m.get_admin_group()})")
|
|
b.click("#account-groups")
|
|
b.click(admin_role_sel + f" li:contains({m.get_admin_group()}) > button")
|
|
b.wait(is_admin)
|
|
b.wait_not_present("#account-groups-helper")
|
|
|
|
# Login as jussi and change role admin for itself
|
|
b.logout()
|
|
b.login_and_go("/users", user="jussi", password="bar")
|
|
|
|
# There is only one badge and it is for jussi
|
|
b.wait_text('#current-account-badge', 'Your account')
|
|
b.wait_js_cond('document.querySelector("#current-account-badge").previousSibling.getAttribute("href") === "#/jussi"')
|
|
|
|
# The current account is the first in the list
|
|
b.wait_visible("#accounts-list > tbody :first-child #current-account-badge")
|
|
|
|
# Use [x] button on the group label to remove the account from group
|
|
b.go("#/jussi")
|
|
b.wait_text("#account-user-name", "jussi")
|
|
b.wait_visible(admin_role_sel + f" .pf-v5-c-label:contains({m.get_admin_group()})")
|
|
b.wait_not_present("#account-groups-helper")
|
|
b.click(f".pf-v5-c-label-group__list .pf-v5-c-label__content:contains({m.get_admin_group()}) + span > button[aria-label='Close {m.get_admin_group()}']")
|
|
b.wait(lambda: not is_admin())
|
|
b.wait_not_present(admin_role_sel + f" .pf-v5-c-label:contains({m.get_admin_group()})")
|
|
if not m.ostree_image: # User is not shown as logged in when logged in through Cockpit
|
|
b.wait_visible("#account-groups-helper")
|
|
m.execute(f"/usr/bin/gpasswd -a jussi {m.get_admin_group()}")
|
|
with b.wait_timeout(20):
|
|
b.wait_visible(admin_role_sel + f" .pf-v5-c-label:contains({m.get_admin_group()})")
|
|
|
|
# Cannot lock the current account
|
|
b.wait_visible("#account-locked[disabled]")
|
|
|
|
b.go("#/admin")
|
|
b.wait_text("#account-user-name", "admin")
|
|
b.wait_visible(admin_role_sel + f" .pf-v5-c-label:contains({m.get_admin_group()})")
|
|
b.wait_not_present("#account-groups-helper")
|
|
b.logout()
|
|
b.login_and_go("/users")
|
|
|
|
# Change the password of this account
|
|
b.go("#/jussi")
|
|
b.wait_text("#account-user-name", "jussi")
|
|
b.click('#account-set-password')
|
|
b.wait_visible('#account-set-password-dialog')
|
|
|
|
# weak password
|
|
b.set_input_text("#account-set-password-pw1", 'a')
|
|
b.set_input_text("#account-set-password-pw2", 'a')
|
|
b.wait_visible("#account-set-password-meter.danger")
|
|
b.click('#account-set-password-dialog button.apply')
|
|
b.wait_in_text("#account-set-password-dialog .pf-v5-c-form__helper-text .pf-m-warning", "Password quality check failed:")
|
|
b.wait_visible('#account-set-password-dialog button.pf-m-warning')
|
|
|
|
# password mismatch
|
|
b.set_input_text("#account-set-password-pw1", good_password + 'a')
|
|
b.set_input_text("#account-set-password-pw2", good_password + 'b')
|
|
b.click('#account-set-password-dialog button.apply')
|
|
b.wait_in_text("#account-set-password-dialog .pf-v5-c-form__helper-text .pf-m-error", "The passwords do not match")
|
|
b.wait_not_present('#account-set-password-dialog button.pf-m-warning')
|
|
|
|
# too long password
|
|
long_password = "2a02-x!h4a" * 30
|
|
b.set_input_text('#account-set-password-pw1', long_password)
|
|
b.set_input_text('#account-set-password-pw2', long_password)
|
|
b.wait_not_present("#account-set-password-dialog .pf-v5-c-form__helper-text .pf-m-warning")
|
|
b.click('#account-set-password-dialog button.apply')
|
|
b.wait_in_text("#account-set-password-dialog .pf-v5-c-form__helper-text .pf-m-warning", "Password is longer than 256 characters")
|
|
b.wait_not_present('#account-set-password-dialog button.pf-m-warning')
|
|
|
|
good_password_2 = "cEwghLY§X9R&m8RLwk4Xfed9Bw="
|
|
# Now set to something valid
|
|
b.set_input_text("#account-set-password-pw1", good_password_2)
|
|
b.set_input_text("#account-set-password-pw2", good_password_2)
|
|
b.wait_visible("#account-set-password-meter.success")
|
|
b.wait_not_present("#account-set-password-dialog .pf-v5-c-form__helper-text .pf-m-warning")
|
|
b.click('#account-set-password-dialog button.apply')
|
|
b.wait_not_present('#account-set-password-dialog')
|
|
|
|
# incomplete passwd entry; fixed in PR #13384
|
|
m.execute('echo "damaged:x:1234:1234:Damaged" >> /etc/passwd')
|
|
|
|
# Logout and login with the new password
|
|
b.relogin(path="/users", user="jussi", password=good_password_2)
|
|
|
|
b.go("/users")
|
|
b.enter_page("/users")
|
|
b.wait_in_text('#accounts-list', "damaged")
|
|
b.click('#accounts-list td[data-label="Username"] a[href="#/damaged"]')
|
|
b.wait_in_text("#account-title", "Damaged")
|
|
|
|
if not m.ostree_image: # User is not shown as logged in when logged in through Cockpit
|
|
b.go("#/admin")
|
|
b.wait_visible("#account-logout[disabled]")
|
|
|
|
(year, month) = m.execute("date +'%Y %b'").strip().split()
|
|
|
|
# Log in as "admin" and the open details in other browser should update
|
|
b2 = self.new_browser(m)
|
|
b2.login_and_go("/system")
|
|
b.wait_text("#account-last-login", "Logged in")
|
|
b.wait_visible("#account-logout:not(:disabled)")
|
|
|
|
# Now log out and it should update again
|
|
b2.logout()
|
|
b.wait_in_text("#account-last-login", year)
|
|
b.wait_in_text("#account-last-login", month)
|
|
b.wait_visible("#account-logout[disabled]")
|
|
|
|
# Terminate session
|
|
b2.login_and_go("/system")
|
|
b.wait_text("#account-last-login", "Logged in")
|
|
b.click("#account-details button:contains('Terminate session')")
|
|
b.wait_in_text("#account-last-login", year)
|
|
b.wait_in_text("#account-last-login", month)
|
|
b.wait_visible("#account-logout[disabled]")
|
|
|
|
# Create an account and force password change on first login
|
|
b.go('/users')
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="robert",
|
|
real_name="Robert Robertson",
|
|
password=good_password,
|
|
locked=False,
|
|
force_password_change=True,
|
|
)
|
|
# Login as robert and check if password change is required
|
|
b.logout()
|
|
|
|
# login in second window to check if last login is updated in accounts list
|
|
if not m.ostree_image: # User is not shown as logged in when logged in through Cockpit
|
|
b2.login_and_go("/users")
|
|
b2.wait_in_text("#accounts-list tbody tr:contains(robert) td[data-label='Last active']", "Never logged in")
|
|
|
|
# On OSTree this happens over ssh
|
|
if m.ostree_image:
|
|
self.restore_dir("/etc/ssh", '( ! systemctl is-active sshd.socket || systemctl stop sshd.socket) && systemctl restart sshd.service')
|
|
m.execute("sed -i 's/.*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*")
|
|
m.execute("( ! systemctl is-active sshd.socket || systemctl stop sshd.socket) && systemctl restart sshd.service")
|
|
|
|
b.wait_visible("#login")
|
|
b.wait_not_visible("#conversation-group")
|
|
b.try_login(user="robert", password=good_password)
|
|
b.wait_visible('#conversation-input')
|
|
b.set_val('#conversation-input', good_password)
|
|
b.click('#login-button')
|
|
|
|
# Set new password
|
|
b.wait_in_text('#conversation-prompt', "New password:")
|
|
b.set_val('#conversation-input', good_password_2)
|
|
b.click('#login-button')
|
|
|
|
# Confirm new password
|
|
b.wait_in_text('#conversation-prompt', "Retype new password:")
|
|
b.set_val('#conversation-input', good_password_2)
|
|
b.click('#login-button')
|
|
b.wait_visible('#content')
|
|
|
|
def testCustomUID(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
self.login_and_go("/users")
|
|
|
|
# Test custom UID
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="bob",
|
|
real_name="Bob Bobson",
|
|
password=good_password,
|
|
locked=False,
|
|
force_password_change=False,
|
|
uid="1500",
|
|
verify_created=True,
|
|
)
|
|
b.wait_visible("#accounts-list td[data-label='Username']:contains('bob') + td + td:contains('1500')")
|
|
|
|
# Test dialog predicts corrent next available UID
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="john",
|
|
real_name="John Johnson",
|
|
password=good_password,
|
|
locked=False,
|
|
force_password_change=False,
|
|
expected_uid="1501",
|
|
verify_created=True,
|
|
)
|
|
b.wait_visible("#accounts-list td[data-label='Username']:contains(john) + td + td:contains(1501)")
|
|
|
|
# Test creation of users with the same UID
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="jack",
|
|
real_name="Jack Jackson",
|
|
password=good_password,
|
|
locked=False,
|
|
force_password_change=False,
|
|
uid="1501",
|
|
verify_created=False,
|
|
)
|
|
b.wait_visible("#accounts-create-dialog")
|
|
b.wait_in_text("#accounts-create-user-uid-helper", "already used")
|
|
b.wait_visible("button.apply:disabled")
|
|
b.click("button:contains('Create account with non-unique UID')")
|
|
b.wait_not_present("#accounts-create-dialog")
|
|
b.wait_in_text("#accounts-list", "Jack Jackson")
|
|
b.wait_visible("#accounts-list td[data-label='Username']:contains(jack) + td + td:contains(1501)")
|
|
b.wait_visible("#accounts-list td[data-label='Username']:contains(john) + td + td:contains(1501)")
|
|
|
|
# No UID specified -> useradd chooses UID for us
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="nouidspecified",
|
|
real_name="NoUID Specified",
|
|
password=good_password,
|
|
locked=False,
|
|
force_password_change=False,
|
|
uid="",
|
|
verify_created=True,
|
|
)
|
|
|
|
# UID cannot be lower than UID_MIN
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="failedfailson",
|
|
real_name="Failed Failson",
|
|
password=good_password,
|
|
locked=False,
|
|
force_password_change=False,
|
|
uid="1",
|
|
verify_created=False,
|
|
)
|
|
b.wait_visible("#accounts-create-dialog")
|
|
b.wait_in_text("#accounts-create-user-uid-helper", "lower than")
|
|
b.click("#accounts-create-dialog button.cancel")
|
|
b.wait_not_present("#accounts-create-dialog")
|
|
|
|
# UID cannot be higher than UID_MAX
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="failedfailson",
|
|
real_name="Failed Failson",
|
|
password=good_password,
|
|
locked=False,
|
|
force_password_change=False,
|
|
uid="9999999",
|
|
verify_created=False,
|
|
)
|
|
b.wait_visible("#accounts-create-dialog")
|
|
b.wait_in_text("#accounts-create-user-uid-helper", "higher than")
|
|
b.click("#accounts-create-dialog button.cancel")
|
|
b.wait_not_present("#accounts-create-dialog")
|
|
|
|
# UID must be a positive integer
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="failedfailson",
|
|
real_name="Failed Failson",
|
|
password=good_password,
|
|
locked=False,
|
|
force_password_change=False,
|
|
uid="abc",
|
|
verify_created=False,
|
|
)
|
|
b.wait_visible("#accounts-create-dialog")
|
|
b.wait_in_text("#accounts-create-user-uid-helper", "positive integer")
|
|
b.click("#accounts-create-dialog button.cancel")
|
|
b.wait_not_present("#accounts-create-dialog")
|
|
|
|
def testCustomUserProperties(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
self.login_and_go("/users")
|
|
|
|
# Test custom home directory
|
|
custom_dir_path = "/home/mycustomdir"
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="jussi",
|
|
real_name="Jussi Junior",
|
|
password=good_password,
|
|
locked=False,
|
|
custom_home_dir=custom_dir_path,
|
|
force_password_change=False,
|
|
verify_created=True,
|
|
)
|
|
|
|
b.go("#/jussi")
|
|
b.wait_text("#account-home-dir", custom_dir_path)
|
|
m.execute(f"test -d {custom_dir_path}")
|
|
m.execute("! test -d /home/jussi")
|
|
|
|
b.go("/users")
|
|
# Check assigning a file as home directory fails
|
|
m.execute("touch /home/isfile")
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="file",
|
|
real_name="File Filerson",
|
|
password=good_password,
|
|
locked=False,
|
|
custom_home_dir="/home/isfile",
|
|
force_password_change=False,
|
|
verify_created=False,
|
|
)
|
|
b.wait_visible("#accounts-create-dialog")
|
|
b.wait_in_text("#accounts-create-user-home-dir-helper", "existing file")
|
|
b.wait_visible("button.apply:disabled")
|
|
b.click("button.cancel")
|
|
b.wait_not_present("#accounts-create-dialog")
|
|
|
|
b.go("/users")
|
|
# Check assigning existing home directory to a new user
|
|
m.execute("mkdir /home/existingdir")
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="stealer",
|
|
real_name="Stealer OfHomeDirectison",
|
|
password=good_password,
|
|
locked=False,
|
|
custom_home_dir="/home/existingdir",
|
|
force_password_change=False,
|
|
verify_created=False,
|
|
)
|
|
b.wait_visible("#accounts-create-dialog")
|
|
b.wait_in_text("#accounts-create-user-home-dir-helper", "already exists")
|
|
b.wait_visible("button.apply:disabled")
|
|
self.assertEqual(m.execute("stat -c '%U %G' /home/existingdir").rstrip(), "root root")
|
|
b.click("button:contains('Create and change ownership of home directory')")
|
|
b.wait_not_present("#accounts-create-dialog")
|
|
b.wait_in_text("#accounts-list", "Stealer OfHomeDirectison")
|
|
# Verify that ownership of home directory was changed to the new user
|
|
self.assertEqual(m.execute("stat -c '%U %G' /home/existingdir").rstrip(), "stealer stealer")
|
|
|
|
default_shell = getUserAddDetails(m)["SHELL"]
|
|
custom_shell = "/bin/sh"
|
|
if default_shell == custom_shell:
|
|
custom_shell = "/bin/bash"
|
|
|
|
createUser(
|
|
browser=b,
|
|
machine=m,
|
|
user_name="robert",
|
|
real_name="Robert Robertson",
|
|
password=good_password,
|
|
locked=False,
|
|
custom_shell=custom_shell,
|
|
force_password_change=False,
|
|
verify_created=True,
|
|
)
|
|
|
|
b.go("#/robert")
|
|
b.wait_text("#account-shell", custom_shell)
|
|
|
|
def testUnprivileged(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
new_password = "tqymuVh.Zf5"
|
|
new_password_2 = "cEwghLYX"
|
|
|
|
m.execute("useradd antoine; echo antoine:foobar | chpasswd")
|
|
self.login_and_go("/users", user="antoine", superuser=False)
|
|
b.go("#/antoine")
|
|
b.wait_text("#account-user-name", "antoine")
|
|
b.wait_visible('#account-set-password:enabled')
|
|
b.click('#account-set-password')
|
|
b.wait_visible('#account-set-password-dialog')
|
|
b.set_input_text("#account-set-password-old", "foobar")
|
|
b.set_input_text("#account-set-password-pw1", new_password)
|
|
b.set_input_text("#account-set-password-pw2", new_password)
|
|
b.click('#account-set-password-dialog button.apply')
|
|
b.wait_not_present('#account-set-password-dialog')
|
|
|
|
# Logout and login with the new password
|
|
b.logout()
|
|
b.open("/users")
|
|
b.wait_visible("#login")
|
|
b.set_val("#login-user-input", "antoine")
|
|
b.set_val("#login-password-input", new_password)
|
|
b.click('#login-button')
|
|
b.wait_visible('#content')
|
|
|
|
# Set minimum age to disallow changing it immediately again
|
|
m.execute("chage --mindays 7 antoine")
|
|
b.enter_page("/users")
|
|
b.go("#/antoine")
|
|
b.wait_text("#account-user-name", "antoine")
|
|
b.wait_visible('#account-set-password:enabled')
|
|
b.click('#account-set-password')
|
|
b.wait_visible('#account-set-password-dialog')
|
|
b.set_input_text("#account-set-password-old", new_password)
|
|
b.set_input_text("#account-set-password-pw1", new_password_2)
|
|
b.set_input_text("#account-set-password-pw2", new_password_2)
|
|
b.click('#account-set-password-dialog button.apply')
|
|
b.wait_in_text("#account-set-password-dialog .pf-v5-c-modal-box__body", "must wait longer")
|
|
|
|
@skipOstree("ssh root login not allowed")
|
|
def testRootLogin(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
new_password = "tqymuVh.Zf5"
|
|
|
|
# this test uses quick logouts; async preloads cause "ReferenceError: cockpit is not defined"
|
|
self.disable_preload("packagekit", "playground", "systemd")
|
|
|
|
m.execute("useradd anton; echo anton:foobar | chpasswd")
|
|
self.enable_root_login()
|
|
self.login_and_go("/users", user="root", superuser=False)
|
|
|
|
# test this on root and a normal user account
|
|
for user in ["anton", "root"]:
|
|
b.go("#/" + user)
|
|
b.wait_text("#account-user-name", user)
|
|
b.wait_visible('#account-set-password:enabled')
|
|
b.click('#account-set-password')
|
|
b.wait_visible('#account-set-password-dialog')
|
|
b.wait_visible("#account-set-password-pw1")
|
|
# root does not need to know old password
|
|
b.wait_not_present("#account-set-password-old")
|
|
b.set_input_text("#account-set-password-pw1", new_password)
|
|
b.set_input_text("#account-set-password-pw2", new_password)
|
|
b.click('#account-set-password-dialog button.apply')
|
|
b.wait_not_present('#account-set-password-dialog')
|
|
|
|
# Logout and login with the new password
|
|
for user in ["anton", "root"]:
|
|
b.logout()
|
|
b.open("/users")
|
|
b.wait_visible("#login")
|
|
b.set_val("#login-user-input", user)
|
|
b.set_val("#login-password-input", new_password)
|
|
b.click('#login-button')
|
|
b.wait_visible('#content')
|
|
|
|
def accountExpiryInfo(self, account, field):
|
|
for line in self.machine.execute(f"LC_ALL=C chage -l {account}").split("\n"):
|
|
if line.startswith(field):
|
|
_, _, value = line.partition(":")
|
|
return value.strip()
|
|
return None
|
|
|
|
def testExpire(self):
|
|
m = self.machine
|
|
b = self.browser
|
|
|
|
m.execute("useradd scruffy -s /bin/bash -c Scruffy")
|
|
m.execute("echo scruffy:foobar | chpasswd")
|
|
|
|
self.login_and_go("/users")
|
|
b.go("#/scruffy")
|
|
b.wait_text("#account-user-name", "scruffy")
|
|
|
|
# Try to expire the account
|
|
b.wait_text("#account-expiration-text", "Never expire account")
|
|
self.assertEqual(self.accountExpiryInfo("scruffy", "Account expires"), "never")
|
|
b.click("#account-expiration-button")
|
|
b.wait_visible("#account-expiration")
|
|
b.click("#account-expiration-expires")
|
|
|
|
# Try an invalid date
|
|
b.set_input_text("#account-expiration-input input", "blah")
|
|
b.click("#account-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
|
|
b.wait_text("#account-expiration .pf-v5-c-form__helper-text .pf-m-error", "Invalid expiration date")
|
|
|
|
# Now a valid date 30 days in the future
|
|
when = datetime.datetime.now() + datetime.timedelta(days=30)
|
|
b.set_input_text("#account-expiration-input input", when.isoformat().split("T")[0])
|
|
b.click("#account-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
|
|
b.wait_not_present("#account-expiration")
|
|
b.wait_in_text("#account-expiration-text", "Expire account on")
|
|
self.assertNotEqual(self.accountExpiryInfo("scruffy", "Account expires"), "never")
|
|
|
|
# Now try and change it back
|
|
b.click("#account-expiration-button")
|
|
b.wait_visible("#account-expiration")
|
|
b.click("#account-expiration-never")
|
|
b.click("#account-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
|
|
b.wait_not_present("#account-expiration")
|
|
b.wait_text("#account-expiration-text", "Never expire account")
|
|
self.assertEqual(self.accountExpiryInfo("scruffy", "Account expires"), "never")
|
|
|
|
# Try to expire a password
|
|
b.wait_text("#password-expiration-text", "Never expire password")
|
|
self.assertEqual(self.accountExpiryInfo("scruffy", "Password expires"), "never")
|
|
b.click("#password-expiration-button")
|
|
b.wait_visible("#password-expiration")
|
|
b.click("#password-expiration-expires")
|
|
|
|
# Try an invalid number
|
|
b.set_input_text("#password-expiration-input", "-3")
|
|
b.click("#password-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
|
|
b.wait_text("#password-expiration .pf-v5-c-form__helper-text .pf-m-error", "Invalid number of days")
|
|
|
|
# Expire password every 30 days
|
|
b.set_input_text("#password-expiration-input", "30")
|
|
b.click("#password-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
|
|
b.wait_not_present("#password-expiration")
|
|
b.wait_in_text("#password-expiration-text", "Require password change on")
|
|
self.assertNotEqual(self.accountExpiryInfo("scruffy", "Password expires"), "never")
|
|
|
|
# Now try and change it back
|
|
b.click("#password-expiration-button")
|
|
b.wait_visible("#password-expiration")
|
|
b.click("#password-expiration-never")
|
|
b.click("#password-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
|
|
b.wait_not_present("#password-expiration")
|
|
b.wait_text("#password-expiration-text", "Never expire password")
|
|
self.assertEqual(self.accountExpiryInfo("scruffy", "Password expires"), "never")
|
|
|
|
# Now change it to expire again
|
|
b.click("#password-expiration-button")
|
|
b.wait_visible("#password-expiration")
|
|
b.click("#password-expiration-expires")
|
|
b.set_input_text("#password-expiration-input", "30")
|
|
b.click("#password-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
|
|
b.wait_not_present("#password-expiration")
|
|
|
|
b.logout()
|
|
self.login_and_go("/users", user="scruffy")
|
|
b.go("#/scruffy")
|
|
b.wait_text("#account-user-name", "scruffy")
|
|
b.wait_text("#account-expiration-text", "Never expire account")
|
|
b.wait_visible("#account-expiration-button[disabled]")
|
|
b.wait_in_text("#password-expiration-text", "Require password change on")
|
|
b.wait_visible("#password-expiration-button[disabled]")
|
|
|
|
# Lastly force a password change
|
|
b.logout()
|
|
self.login_and_go("/users")
|
|
b.go("#/scruffy")
|
|
b.wait_text("#account-user-name", "scruffy")
|
|
b.click("#password-reset-button")
|
|
b.wait_visible("#password-reset")
|
|
b.click("#password-reset .pf-v5-c-modal-box__footer button:contains(Reset)")
|
|
b.wait_not_present("password-reset")
|
|
b.wait_in_text("#password-expiration-text", "Password must be changed")
|
|
self.assertEqual(self.accountExpiryInfo("scruffy", "Password expires"), "password must be changed")
|
|
|
|
@skipOstree("User is not shown as logged in when logged in through Cockpit")
|
|
def testAccountLogs(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
# Clean out the relevant logfiles
|
|
m.execute("truncate -s0 /var/log/{[bw]tmp,lastlog} /var/run/utmp")
|
|
|
|
# Login once to create an entry
|
|
self.login_and_go("/users")
|
|
b.logout()
|
|
|
|
self.login_and_go("/users")
|
|
b.go("#/admin")
|
|
b.wait_visible("#account-logs")
|
|
# Header + one line of logins
|
|
b.wait_js_func("ph_count_check", "#account-logs tr", 2)
|
|
|
|
def testGroups(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
def performGroupAction(browser, group, action):
|
|
browser.click(f"#groups-list tbody tr:contains({group}) .pf-v5-c-dropdown button")
|
|
browser.click(f"#groups-list tbody tr:contains({group}) .pf-v5-c-dropdown__menu li:contains({action})")
|
|
|
|
def selectGroupFromMenu(group, enabled):
|
|
if enabled:
|
|
wait(lambda: "testgroup0" not in m.execute("groups admin"))
|
|
else:
|
|
wait(lambda: "testgroup0" in m.execute("groups admin"))
|
|
|
|
b.wait_not_present(".pf-v5-c-select__menu")
|
|
b.click("#account-groups")
|
|
b.click(f".pf-v5-c-select__menu li:contains({group}) button")
|
|
if enabled:
|
|
b.wait_in_text(".pf-v5-c-label-group__list", group)
|
|
b.wait_not_present(f".pf-v5-c-select__menu li:contains({group}) button")
|
|
wait(lambda: "testgroup0" in m.execute("groups admin"))
|
|
else:
|
|
b.wait_not_in_text(".pf-v5-c-label-group__list", group)
|
|
b.wait_not_present(f".pf-v5-c-select__menu li:contains({group}) button")
|
|
wait(lambda: "testgroup0" not in m.execute("groups admin"))
|
|
|
|
m.execute("groupadd testgroup0")
|
|
m.execute("useradd anton")
|
|
|
|
self.login_and_go("/users")
|
|
|
|
# Groups filter is only visible in the expanded view
|
|
b.wait_not_present("#groups-filter")
|
|
|
|
b.click("#groups-view-toggle")
|
|
b.wait_visible('#groups-list td[data-label="Group name"]:contains("testgroup0")')
|
|
|
|
# Check group filter in expanded mode and the filter clear button
|
|
b.set_input_text("#groups-filter input", "casablanca")
|
|
b.wait_not_present('#groups-list td[data-label="Group name"]:contains("testgroup0")')
|
|
b.click("#groups-filter button")
|
|
b.wait_val("#groups-filter input", "")
|
|
b.wait_visible('#groups-list td[data-label="Group name"]:contains("testgroup0")')
|
|
|
|
# FIXME: rtl test was flaky, debug it and remove the skip
|
|
b.assert_pixels("#groups-list tr:contains(testgroup0)", "group-row", skip_layouts=["rtl"])
|
|
|
|
# Delete it
|
|
performGroupAction(b, 'testgroup0', 'Delete group')
|
|
b.wait_text("#group-confirm-delete-dialog footer .pf-v5-c-button.apply", "Delete")
|
|
b.click("#group-confirm-delete-dialog footer .pf-v5-c-button.apply")
|
|
b.wait_not_present('#group-confirm-delete-dialog')
|
|
b.wait_visible("#groups-list")
|
|
b.wait_not_in_text('#groups-list', "testgroup0")
|
|
|
|
# Add testgroup0 back
|
|
m.execute("groupadd testgroup0")
|
|
|
|
# Groups used as primary need force deletion
|
|
performGroupAction(b, 'anton', 'Delete group')
|
|
b.wait_text("#group-confirm-delete-dialog footer .pf-v5-c-button.apply", "Force delete")
|
|
b.assert_pixels("#group-confirm-delete-dialog", "group-delete-dialog")
|
|
b.click("#group-confirm-delete-dialog footer .pf-v5-c-button.apply")
|
|
b.wait_not_present('#account-confirm-delete-dialog')
|
|
b.wait(lambda: "#/" == b.eval_js('window.location.hash'))
|
|
b.wait_visible("#groups-list")
|
|
b.wait_not_in_text('#groups-list', "anton")
|
|
|
|
b.click('#accounts-list td[data-label="Username"] a[href="#/admin"]')
|
|
|
|
# Existing groups appear in labels
|
|
b.wait_in_text(".pf-v5-c-label-group__list", "admin")
|
|
b.wait_in_text(".pf-v5-c-label-group__list", m.get_admin_group())
|
|
|
|
# Primary group cannot be remove but others have a remove button
|
|
b.wait_visible(".pf-v5-c-label-group__list .pf-v5-c-label__content:contains(admin)")
|
|
b.wait_not_present(".pf-v5-c-label-group__list .pf-v5-c-label__content:contains(admin) + span > button")
|
|
b.wait_visible(f".pf-v5-c-label-group__list .pf-v5-c-label__content:contains({m.get_admin_group()}) + span > button[aria-label='Close {m.get_admin_group()}']")
|
|
|
|
# Clicking on the close button removes the group
|
|
b.click(f".pf-v5-c-label-group__list .pf-v5-c-label__content:contains({m.get_admin_group()}) + span > button[aria-label='Close {m.get_admin_group()}']")
|
|
b.wait_not_present(f".pf-v5-c-label-group__list .pf-v5-c-label__content:contains({m.get_admin_group()})")
|
|
b.wait_not_present(".pf-v5-c-select__menu")
|
|
|
|
# Add admin to the testgroup0 group
|
|
selectGroupFromMenu("testgroup0", True)
|
|
|
|
# Check that changes ar persistent after reload
|
|
b.reload()
|
|
b.enter_page("/users")
|
|
b.wait_in_text(".pf-v5-c-label-group__list", "testgroup0")
|
|
|
|
# Clicking on a used groups in the menu will remove it
|
|
selectGroupFromMenu("testgroup0", False)
|
|
|
|
# Clicking on the undo button will add the removed group back
|
|
b.click("#group-undo-btn")
|
|
b.wait_in_text(".pf-v5-c-label-group__list", "testgroup0")
|
|
m.execute("/usr/bin/gpasswd -d admin testgroup0")
|
|
|
|
# Clicking on the undo button will remove the added group back
|
|
b.reload()
|
|
b.enter_page("/users")
|
|
selectGroupFromMenu("testgroup0", True)
|
|
b.click("#group-undo-btn")
|
|
b.wait_not_in_text(".pf-v5-c-label-group__list", "testgroup0")
|
|
wait(lambda: "testgroup0" not in m.execute("groups admin"))
|
|
|
|
def testGroupCreate(self):
|
|
b = self.browser
|
|
|
|
self.login_and_go("/users")
|
|
|
|
# Create new group
|
|
b.click("#groups-create")
|
|
b.set_input_text("#groups-create-name", "titan")
|
|
b.wait_not_val("#groups-create-id", "")
|
|
b.click("#groups-create-dialog button.pf-m-primary")
|
|
b.wait_not_present("#groups-create-dialog")
|
|
b.wait_visible('#groups-list td[data-label="Group name"]:contains("titan")')
|
|
|
|
# Validation check for duplicate group name and id
|
|
b.click("#groups-create")
|
|
b.set_input_text("#groups-create-name", "titan")
|
|
b.set_input_text("#groups-create-id", "0")
|
|
b.click("#groups-create-dialog button.pf-m-primary")
|
|
b.wait_in_text("#groups-create-name-helper", "A group with this name already exists")
|
|
b.set_input_text("#groups-create-name", "titans")
|
|
b.click("#groups-create-dialog button.pf-m-primary")
|
|
b.wait_in_text("#groups-create-dialog .pf-v5-c-alert", "GID '0' already exists")
|
|
|
|
# Validation check for chars used in group name and valid group ID
|
|
b.set_input_text("#groups-create-name", "titan@1000")
|
|
b.set_input_text("#groups-create-id", "12f")
|
|
b.click("#groups-create-dialog button.pf-m-primary")
|
|
b.wait_in_text("#groups-create-name-helper", "The group name can only consist of letters")
|
|
b.wait_in_text("#groups-create-id-helper", "The group ID must be positive integer")
|
|
b.set_input_text("#groups-create-id", "-12")
|
|
b.wait_not_present("#groups-create-id-helper")
|
|
b.click("#groups-create-dialog button.pf-m-primary")
|
|
b.wait_in_text("#groups-create-id-helper", "The group ID must be positive integer")
|
|
|
|
# Validate no name and no group
|
|
b.set_input_text("#groups-create-name", "")
|
|
b.set_input_text("#groups-create-id", "")
|
|
b.click("#groups-create-dialog button.pf-m-primary")
|
|
b.wait_in_text("#groups-create-name-helper", "No group name specified")
|
|
b.wait_in_text("#groups-create-id-helper", "No ID specified")
|
|
|
|
# Create new group with custom ID
|
|
b.set_input_text("#groups-create-name", "saturn")
|
|
b.set_input_text("#groups-create-id", "1234")
|
|
b.click("#groups-create-dialog button.pf-m-primary")
|
|
b.wait_not_present("#groups-create-dialog")
|
|
b.wait_visible('#groups-list td[data-label="Group name"]:contains("saturn")')
|
|
|
|
def testGroupRename(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
m.execute("groupadd titan; useradd uranus")
|
|
|
|
self.login_and_go("/users")
|
|
b.click("button:contains('more...')")
|
|
b.wait_visible('#groups-list td[data-label="Group name"]:contains("titan")')
|
|
b.click("#groups-list tbody tr:contains(titan) .pf-v5-c-dropdown button")
|
|
b.click("#groups-list tbody tr:contains(titan) .pf-v5-c-dropdown__menu li:contains('Rename group')")
|
|
|
|
b.set_input_text("#group-confirm-rename-dialog #group-name", "phoebe")
|
|
b.click("#group-confirm-rename-dialog .apply")
|
|
b.wait_not_present("#group-confirm-rename-dialog")
|
|
self.assertIn("phoebe", m.execute("getent group"))
|
|
self.assertNotIn("titan", m.execute("getent group"))
|
|
|
|
# Rename and delete actions are available only for user created groups
|
|
b.wait_not_present(f"#groups-list tbody tr:contains({m.get_admin_group()}) .pf-v5-c-dropdown button")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
test_main()
|