#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv) # # Copyright (C) 2013 Red Hat, Inc. # SPDX-License-Identifier: LGPL-2.1-or-later import datetime import os import testlib from machine import testvm good_password = "tqymuVh.ZfZnP§9Wr=LM3JyG5yx" SHADOW_UTILS_WITHOUT_EXPIRE = ["arch", "debian-testing", "fedora-45"] def getUserAddDetails(machine: testvm.Machine) -> dict[str, str]: useradd_defaults: dict[str, str] = {} for line in machine.execute("useradd -D").splitlines(): key, value = line.split("=") useradd_defaults[key] = value return useradd_defaults def performUserAction(browser: testlib.Browser, user: str, action: str) -> None: browser.click(f"#accounts-list tr[data-row-id='{user}'] button.pf-v6-c-menu-toggle") browser.wait_visible(f"#accounts-list tr[data-row-id='{user}'] button.pf-v6-c-menu-toggle[aria-expanded=true]") browser.click(f".pf-v6-c-menu button.pf-v6-c-menu__item:contains({action})") def performGroupAction(browser: testlib.Browser, group: str, action: str) -> None: browser.click(f"#groups-list tbody tr[data-row-id='{group}'] button.pf-v6-c-menu-toggle") browser.click(f".pf-v6-c-menu button.pf-v6-c-menu__item:contains({action})") def createUser( browser: testlib.Browser, machine: testvm.Machine, user_name: str, real_name: str, locked: bool, force_password_change: bool, password: str | None = None, custom_home_dir: str | None = None, default_shell: str | None = None, custom_shell: str | None = None, uid: str | None = None, expected_uid: str | None = None, verify_created: bool = True, run_assert_pixels: bool = False, ) -> None: if default_shell is None: default_shell = getUserAddDetails(machine)["SHELL"] or "/bin/bash" 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) @testlib.nondestructive @testlib.skipImage("TODO: fix for OpenSUSE", "opensuse-tumbleweed") class TestAccounts(testlib.MachineCase): def testBasic(self) -> None: b = self.browser m = self.machine # Clean out the relevant logfiles m.execute("truncate -s0 /var/log/{[bw]tmp{,.db},lastlog} /run/utmp") m.execute("rm -f /var/lib/lastlog/lastlog2.db /var/lib/wtmpdb/wtmp.db") 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.wait_visible("#account-locked:not([disabled])") b.wait_text("#account-user-name", "anton") b.wait_text("#account-title", "anton") b.wait_text("#account-last-login", "Never") b.assert_pixels("#users-page", "user-detail-page") 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', 'foo:bar') b.wait_in_text('#account-real-name-helper', 'The full name must not contain colons.') b.set_input_text('#account-real-name', "Anton Arbitrary") b.wait_not_present('#account-real-name-helper') 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") m.execute("useradd --create-home barney") m.execute("echo barney:foobar | chpasswd") m.execute("runuser -u barney -- touch /home/barney/test.txt") m.execute("test -f /home/barney/test.txt") with b.wait_timeout(30): b.wait_in_text('#accounts-list', "barney") performUserAction(b, 'barney', 'Delete') b.set_checked("#account-confirm-delete-files", val=True) b.click('#account-confirm-delete-dialog button.apply') b.wait_not_present('#account-confirm-delete-dialog') m.execute("! test -e /home/barney/test.txt") b.wait_not_in_text('#accounts-list', "barney") # 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-v6-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_in_text("#account-shell", "/bin/bash") # can be /usr/bin/bash or /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", val=True) b.wait(lambda: m.execute("passwd -S root").split()[1] in ["L", "LK"]) # Check admin user b.go("#/admin") b.wait_text("#account-user-name", "admin") # delete/logout are disabled for the current user b.wait_visible("#account-delete[disabled]") b.wait_visible("#account-logout[disabled]") # go back to accounts overview, check pf-v6-c-breadcrumb b.click("#account .pf-v6-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") # Test password show/hide functionality b.wait_visible('#accounts-create-password-pw1[type="password"]') b.click("#accounts-create-password-pw1-group button[aria-label='Show password']") b.wait_visible('#accounts-create-password-pw1[type="text"]') b.click("#accounts-create-password-pw1-group button[aria-label='Hide password']") b.wait_visible('#accounts-create-password-pw1[type="password"]') # Test confirmation password show/hide functionality b.wait_visible('#accounts-create-password-pw2[type="password"]') b.click("#accounts-create-password-pw2-group button[aria-label='Show confirmation password']") b.wait_visible('#accounts-create-password-pw2[type="text"]') b.click("#accounts-create-password-pw2-group button[aria-label='Hide confirmation password']") b.wait_visible('#accounts-create-password-pw2[type="password"]') # 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-v6-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-v6-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-v6-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-v6-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") b.wait_not_present("#account-confirm-lock-dialog") # lock option is now disabled b.click("#accounts-list tbody tr[data-row-id='robert'] button.pf-v6-c-menu-toggle") b.wait_visible(".pf-v6-c-menu__list-item.pf-m-disabled:contains('Lock account')") b.click("#accounts-list tbody tr[data-row-id='robert'] button.pf-v6-c-menu-toggle") # 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") # test logout other user createUser( browser=b, machine=m, user_name="paul", real_name="Paul Robertson", password=good_password, locked=False, force_password_change=False, custom_shell="/bin/bash", # CoreOS defaults to /bin/true ) b2 = self.new_browser(m) b2.login_and_go("/system", user="paul", password=good_password) b.wait_in_text("#accounts-list tbody tr:contains('paul')", "Logged in") performUserAction(b, 'paul', 'Log user out') b.click("#account-confirm-logout-dialog footer .pf-m-primary") b2.switch_to_top() b2.wait_in_text(".curtains-ct h1", "Disconnected") # HACK: lastlog does not record sessions from ssh if not m.ws_container: today = b.eval_js("Intl.DateTimeFormat('en', { dateStyle: 'medium' }).format()") b.wait_in_text("#accounts-list tbody tr:contains('paul')", today) 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) -> None: 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[data-row-id='robert'] td[data-label='Group'] .pf-v6-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[data-row-id='robert'] td[data-label='Group'] .pf-v6-c-label.pf-m-blue:contains('users')", "users") m.execute(f"/usr/bin/gpasswd -a robert {m.get_admin_group()}") b.wait_in_text("#accounts-list tbody tr[data-row-id='robert'] td[data-label='Group'] .pf-v6-c-label.pf-m-yellow", m.get_admin_group()) m.execute(f"/usr/bin/gpasswd -d robert {m.get_admin_group()}") b.wait_not_present("#accounts-list tbody tr[data-row-id='robert'] td[data-label='Group'] .pf-v6-c-label.pf-m-yellow") # test filters b.set_input_text("#accounts-filter input", "root") b.wait_in_text("#accounts-list tbody:first-of-type td[data-label='Username']", "root") b.set_input_text("#accounts-filter input", "rOBeRt") b.wait_in_text("#accounts-list tbody:first-of-type 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:first-of-type td[data-label='ID']", uid) b.set_input_text("#accounts-filter input", "spooky") b.wait_visible("#accounts div.pf-v6-c-empty-state") # clear text filters b.click("#accounts-filter button[aria-label='Reset']") b.wait_not_present("#accounts div.pf-v6-c-empty-state") # clear via empty-state action b.set_input_text("#accounts-filter input", "spooky") b.wait_visible("#accounts div.pf-v6-c-empty-state") b.click("div.pf-v6-c-empty-state button.pf-m-link") b.wait_not_present("#accounts div.pf-v6-c-empty-state") b.eval_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: str, invert: bool = False) -> None: # current account is always in the first row b.wait_in_text("#accounts-list tbody:first-of-type 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) 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) -> None: b = self.browser m = self.machine # Clean out the relevant logfiles m.execute("truncate -s0 /var/log/{[bw]tmp{,.db},lastlog} /run/utmp") m.execute("rm -f /var/lib/lastlog/lastlog2.db /var/lib/wtmpdb/wtmp.db") 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_in_text("#accounts-create-password-pw2-helper", "The passwords do not match") b.set_input_text('#accounts-create-password-pw2', "bar") 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_in_text("#accounts-create-password-pw2-helper", "The passwords do not match") b.set_input_text('#accounts-create-password-pw2', "barfoo") 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_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-v6-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-v6-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-v6-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() -> bool: return m.execute("passwd -S jussi | cut -d' ' -f2").strip() in ["L", "LK"] def is_admin() -> bool: 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', val=False) b.wait(lambda: not is_locked()) b.wait_not_present(admin_role_sel + f" .pf-v6-c-label:contains(':{m.get_admin_group()}')") b.click("#account-groups > button") b.click(f"body .pf-v6-c-menu 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-of-type #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_in_text(f"{admin_role_sel} #account-groups .pf-v6-c-label-group", m.get_admin_group()) b.wait_not_present("#account-groups-helper") b.click(f"#account-groups .pf-v6-c-label__actions button[aria-label='Close {m.get_admin_group()}']") b.wait(lambda: not is_admin()) b.wait_not_in_text(f"{admin_role_sel} #account-groups .pf-v6-c-label-group", 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_in_text(f"{admin_role_sel} #account-groups .pf-v6-c-label-group", 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_in_text(f"{admin_role_sel} #account-groups .pf-v6-c-label-group", 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-v6-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-v6-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-v6-c-form__helper-text .pf-m-warning") b.click('#account-set-password-dialog button.apply') b.wait_in_text("#account-set-password-dialog .pf-v6-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-v6-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.ws_container: # 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.ws_container: # 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[data-row-id='robert'] td[data-label='Last active']", "Never logged in") # With the ws container this happens over ssh and we need # ChallengeResponseAuthentication (or # KbdInteractiveAuthentication, it's new name) to be allowed. # Without that, OpenSSH will do the required password change # without PAM and by running "/bin/passwd" instead. # if m.ws_container: self.restore_dir("/etc/ssh", restart_unit=self.sshd_service) m.execute("sed -i 's/.*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' " "/etc/ssh/sshd_config $(ls /etc/ssh/sshd_config.d/* 2>/dev/null || true)") m.execute("sed -i 's/.*KbdInteractiveAuthentication.*/KbdInteractiveAuthentication yes/' " "/etc/ssh/sshd_config $(ls /etc/ssh/sshd_config.d/* 2>/dev/null || true)") m.execute(self.restart_sshd) 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) -> None: 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_text("#accounts-list tr[data-row-id='bob'] td[data-label='ID']", "1500") # Test dialog correctly predicts the 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_text("#accounts-list tr[data-row-id='john'] td[data-label='ID']", "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_text("#accounts-list tr[data-row-id='jack'] td[data-label='ID']", "1501") b.wait_text("#accounts-list tr[data-row-id='john'] td[data-label='ID']", "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) -> None: 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) -> None: 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') # --mindays is being deprecated in "chage" utility, see # https://github.com/shadow-maint/shadow/pull/1482 for deprecation of --mindays and # https://github.com/shadow-maint/shadow/pull/1432 for full plan what is being deprecated # # This will eventually show up in other distributions too if m.image in SHADOW_UTILS_WITHOUT_EXPIRE: return # 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-v6-c-modal-box__body", "must wait longer") @testlib.skipWsContainer("ssh root login not allowed") def testRootLogin(self) -> None: 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') b.logout() # Logout and login with the new password for user in ["anton", "root"]: b.login_and_go("/users", user=user, password=new_password) b.logout() def accountExpiryInfo(self, account: str, field: str) -> str | None: 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) -> None: 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#/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-v6-c-modal-box__footer button:contains('Change')") b.wait_text("#account-expiration .pf-v6-c-form__helper-text .pf-m-error", "Invalid expiration date") # Now a valid date 30 days in the future when = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=30) b.set_input_text("#account-expiration-input input", when.isoformat().split("T")[0]) b.click("#account-expiration .pf-v6-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-v6-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") # The default "never expire" value depends on PASS_MAX_DAYS in login.defs which varies per distro and is unset in Arch Linux. EXPIRE_NEVER_VALUE = m.execute("awk '/^PASS_MAX_DAYS/ {print $2}' /etc/login.defs").strip() or "-1" self.assertEqual(self.accountExpiryInfo("scruffy", "Maximum number of days between password change"), EXPIRE_NEVER_VALUE) 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-v6-c-modal-box__footer button:contains('Change')") b.wait_text("#password-expiration .pf-v6-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-v6-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-v6-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") self.assertEqual(self.accountExpiryInfo("scruffy", "Maximum number of days between password change"), "-1") # 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-v6-c-modal-box__footer button:contains('Change')") b.wait_not_present("#password-expiration") b.logout() # HACK: https://github.com/cockpit-project/cockpit/issues/20262 self.login_and_go("/users#/scruffy", user="scruffy", superuser=False) 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#/scruffy") b.wait_text("#account-user-name", "scruffy") b.click("#password-reset-button") b.wait_visible("#password-reset") b.click("#password-reset .pf-v6-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") @testlib.skipWsContainer("User is not shown as logged in with cockpit/ws") def testAccountLogs(self) -> None: b = self.browser m = self.machine # Clean out the relevant logfiles m.execute("truncate -s0 /var/log/{[bw]tmp{,.db},lastlog} /run/utmp") m.execute("rm -f /var/lib/lastlog/lastlog2.db /var/lib/wtmpdb/wtmp.db") # First login: no entries yet self.login_and_go("/users#/admin") # just the header, nothing else b.wait_text("#account-logs", "Login history") self.assertFalse(b.is_present("#account-logs tr")) b.logout() year = m.execute("date +%Y").strip() # second login: one entry from the first one self.login_and_go("/users#/admin") # Header + one line of logins b.wait_count("#account-logs tr", 2) started = b.text("#account-logs [data-label='Started']") ended = b.text("#account-logs [data-label='Ended']") self.assertIn(year, started) self.assertIn(year, ended) self.assertGreaterEqual(ended, started) # this is the correct IP for our CI, and we don't run this on tmt b.wait_text("#account-logs [data-label='From']", "::ffff:172.27.0.2") def testGroups(self) -> None: b = self.browser m = self.machine def selectGroupFromMenu(group: str, enabled: bool) -> None: if enabled: testlib.wait(lambda: "testgroup0" not in m.execute("groups admin")) else: testlib.wait(lambda: "testgroup0" in m.execute("groups admin")) b.wait_not_present(".pf-v6-c-menu") b.click("#account-groups > button") b.click(f".pf-v6-c-menu li:contains('{group}') button") if enabled: b.wait_in_text(".pf-v6-c-label-group__list", group) b.wait_not_present(f".pf-v6-c-select__menu li:contains('{group}') button") testlib.wait(lambda: "testgroup0" in m.execute("groups admin")) else: b.wait_not_in_text(".pf-v6-c-label-group__list", group) b.wait_not_present(f".pf-v6-c-select__menu li:contains('{group}') button") testlib.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-v6-c-button.apply", "Delete") b.click("#group-confirm-delete-dialog footer .pf-v6-c-button.apply") b.wait_not_present('#group-confirm-delete-dialog') 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-v6-c-button.apply", "Force delete") b.assert_pixels("#group-confirm-delete-dialog", "group-delete-dialog") b.click("#group-confirm-delete-dialog footer .pf-v6-c-button.apply") b.wait_not_present('#account-confirm-delete-dialog') b.wait(lambda: "#/" == b.eval_js('window.location.hash')) 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-v6-c-label-group__list", "admin") b.wait_in_text(".pf-v6-c-label-group__list", m.get_admin_group()) # Primary group cannot be remove but others have a remove button b.wait_in_text("#account-groups .pf-v6-c-label-group", "admin") b.wait_not_present("#account-groups button[aria-label='Close user']") close_button = f"#account-groups button[aria-label='Close {m.get_admin_group()}']" b.wait_visible(close_button) # Clicking on the close button removes the group b.click(close_button) b.wait_not_in_text("#account-groups .pf-v6-c-label-group", m.get_admin_group()) b.wait_not_present(".pf-v6-c-select__menu") # Add admin to the testgroup0 group selectGroupFromMenu("testgroup0", enabled=True) # Check that changes ar persistent after reload b.reload() b.enter_page("/users") b.wait_in_text(".pf-v6-c-label-group__list", "testgroup0") # Clicking on a used groups in the menu will remove it selectGroupFromMenu("testgroup0", enabled=False) # Clicking on the undo button will add the removed group back b.click("#group-undo-btn") b.wait_in_text(".pf-v6-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", enabled=True) b.click("#group-undo-btn") b.wait_not_in_text(".pf-v6-c-label-group__list", "testgroup0") testlib.wait(lambda: "testgroup0" not in m.execute("groups admin")) def testGroupCreate(self) -> None: 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 tr[data-row-id="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-v6-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", "Group name can only consist of letters") b.wait_in_text("#groups-create-id-helper", "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", "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) -> None: 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 tr[data-row-id="titan"]') performGroupAction(b, "titan", "Rename group") b.set_input_text("#group-confirm-rename-dialog #group-name", "foo bar") b.click("#group-confirm-rename-dialog .apply") b.wait_in_text("#group-name-helper", "Group name can only consist of letters") b.set_input_text("#group-confirm-rename-dialog #group-name", "") b.click("#group-confirm-rename-dialog .apply") b.wait_in_text("#group-name-helper", "Group name cannot be empty") b.set_input_text("#group-confirm-rename-dialog #group-name", "phoebe") b.wait_not_present("#group-name-helper") 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[data-row-id='{m.get_admin_group()}'] button.pf-v6-c-menu-toggle") def testChangeShell(self) -> None: 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") b.go("#/anton") b.wait_text("#account-shell", getUserAddDetails(m)["SHELL"]) b.click("#change-shell-button") b.wait_visible("#shell-dialog") new_shell = "/bin/sh" b.select_from_dropdown("#edit-user-shell", new_shell) b.click('#shell-dialog button.apply') b.wait_text("#account-shell", new_shell) if __name__ == '__main__': testlib.test_main()