Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions share/ansible/roles/ci_run/tasks/alpine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
- pkgconf
state: present

- name: Make sure expect is found
ansible.builtin.file:
src: /usr/bin/expect
dest: /bin/expect
state: link

- name: Build configuration
ansible.builtin.command: >
./autogen.sh
Expand Down
1 change: 1 addition & 0 deletions share/ansible/roles/ci_run/tasks/debian.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- name: Ensure dependencies are installed
ansible.builtin.apt:
name:
- expect
- gpg
- libbsd-dev
- libcmocka-dev
Expand Down
19 changes: 19 additions & 0 deletions share/ansible/roles/ci_run/tasks/fedora.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use_backend: dnf4
name:
- dnf-plugins-core
- expect
- gawk
- libcmocka-devel
- systemd-devel
Expand All @@ -19,6 +20,24 @@
register: dnf_result
changed_when: '"Nothing to do" not in dnf_result.stdout'

- name: Temporary workaround to create the newusers PAM service file
ansible.builtin.copy:
src: /etc/pam.d/chfn
dest: /etc/pam.d/newusers
remote_src: yes
owner: root
group: root
mode: '0644'

- name: Temporary workaround to create the groupmems PAM service file
ansible.builtin.copy:
src: /etc/pam.d/chfn
dest: /etc/pam.d/groupmems
remote_src: yes
owner: root
group: root
mode: '0644'

- name: Build configuration
ansible.builtin.command: >
./autogen.sh
Expand Down
1 change: 1 addition & 0 deletions share/ansible/roles/ci_run/tasks/opensuse.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- autoconf
- automake
- diffutils
- expect
- gawk
- gcc
- gettext-tools
Expand Down
131 changes: 130 additions & 1 deletion tests/system/framework/roles/shadow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
from __future__ import annotations

import shlex
from typing import Dict
from typing import Dict, Tuple

from pytest_mh.conn import ProcessLogLevel, ProcessResult

from ..hosts.shadow import ShadowHost
from ..misc.errors import ExpectScriptError
from .base import BaseLinuxRole

__all__ = [
"Shadow",
]

DEFAULT_INTERACTIVE_TIMEOUT: int = 60
"""Default timeout for interactive sessions."""


class Shadow(BaseLinuxRole[ShadowHost]):
"""
Expand Down Expand Up @@ -142,3 +146,128 @@ def chage(self, *args) -> ProcessResult:
self.host.discard_file("/etc/shadow")

return cmd

def newusers(self, *args, users_data: str | None = None) -> ProcessResult:
"""
Update or create new users in batch.

Updates or creates multiple users by reading account information in passwd
format. If `users_data` is provided, it's passed via stdin and takes
precedence; otherwise, the command reads from a file specified in `args`.
"""
if users_data:
cmd_args = " ".join(args)
self.logger.info(f"Creating users from stdin on {self.host.hostname}")
cmd = self.host.conn.run(f"echo '{users_data}' | newusers {cmd_args}", log_level=ProcessLogLevel.Error)
else:
args_dict = self._parse_args(args)
self.logger.info(f'Creating users from "{args_dict["name"]}" on {self.host.hostname}')
cmd = self.host.conn.run("newusers " + args[0], log_level=ProcessLogLevel.Error)

self.host.discard_file("/etc/passwd")
self.host.discard_file("/etc/shadow")
self.host.discard_file("/etc/group")
self.host.discard_file("/etc/gshadow")

return cmd

def groupmems(self, *args, run_as: str = "root", password: str | None = None) -> ProcessResult:
"""
Administer members of a user's primary group.

The groupmems command allows management of group membership lists.
If `run_as` is provided, then the `password` must be provided and the command groupmems run under this user.
If `run_as` isn't provided, then the command is run under `root`.
"""
args_dict = self._parse_args(args)

if run_as == "root":
self.logger.info(f'Administer {args_dict["name"]} group membership as root on {self.host.hostname}')
cmd = self.host.conn.run("groupmems " + args[0], log_level=ProcessLogLevel.Error)
else:
self.logger.info(f'Administer {args_dict["name"]} group membership on {self.host.hostname}')
cmd = self.host.conn.run(
f"echo '{password}' | su - {run_as} -c 'groupmems {args[0]}'", log_level=ProcessLogLevel.Error
)

self.host.discard_file("/etc/group")
self.host.discard_file("/etc/gshadow")

return cmd

def newgrp(self, *args, run_as: str = "root") -> Tuple[ProcessResult, int]:
"""
Log in to a new group.

The newgrp command is used to change the current group ID during a login session.
Returns the process result and the group ID after the change.
"""
args_dict = self._parse_args(args)

self.logger.info(f'Changing {run_as} to group "{args_dict["name"]}" on {self.host.hostname}')

# Use expect to handle the interactive newgrp session
result = self.host.conn.expect(
rf"""
set timeout {DEFAULT_INTERACTIVE_TIMEOUT}
set prompt "\[#\$>\] $"

if {{ "{run_as}" eq "root" }} {{
spawn newgrp {args[0]}
}} else {{
spawn su - {run_as}
expect {{
-re $prompt {{send "newgrp {args[0]}\n"}}
timeout {{puts "expect result: Timeout waiting for su prompt"; exit 201}}
eof {{puts "expect result: Unexpected end of file"; exit 202}}
}}
}}

expect {{
-re $prompt {{send "id -g\n"}}
timeout {{puts "expect result: Timeout waiting for newgrp prompt"; exit 201}}
eof {{puts "expect result: Unexpected end of file"; exit 202}}
}}

expect {{
-re "(\[0-9\]+)" {{
set gid $expect_out(1,string)
send "exit\n"
puts "newgrp_gid:$gid"
}}
timeout {{puts "expect result: Timeout waiting for id output"; exit 201}}
eof {{puts "expect result: Unexpected end of file"; exit 202}}
}}

if {{ "{run_as}" ne "root" }} {{
expect {{
-re $prompt {{
send "exit\n"
}}
timeout {{puts "expect result: Timeout waiting for original su prompt"; exit 201}}
}}
}}

expect {{
eof {{exit 0}}
timeout {{exit 201}}
}}
""",
verbose=False,
)

if result.rc > 200:
raise ExpectScriptError(result.rc)

gid_line = None
for line in result.stdout_lines:
if "newgrp_gid:" in line:
gid_line = line
break

if gid_line is None:
raise ValueError("Current GID is required for newgrp")

current_gid = int(gid_line.split(":")[1])

return result, current_gid
41 changes: 41 additions & 0 deletions tests/system/tests/test_groupmems.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
Test groupmems
"""

from __future__ import annotations

import pytest

from framework.roles.shadow import Shadow
from framework.topology import KnownTopology


@pytest.mark.topology(KnownTopology.Shadow)
def test_groupmems__add_user_as_root(shadow: Shadow):
"""
:title: Add user to group as root user
:setup:
1. Create test user and group
:steps:
1. Add user to group using groupmems as root
2. Check group and gshadow entry
:expectedresults:
1. User is added to group
2. group and gshadow entry values are correct
:customerscenario: False
"""
shadow.useradd("tuser")
shadow.groupadd("tgroup")

shadow.groupmems("-g tgroup -a tuser")

group_entry = shadow.tools.getent.group("tgroup")
assert group_entry is not None, "Group should be found"
assert group_entry.name == "tgroup", "Incorrect groupname"
assert "tuser" in group_entry.members, "User should be member of group"

if shadow.host.features["gshadow"]:
gshadow_entry = shadow.tools.getent.gshadow("tgroup")
assert gshadow_entry is not None, "Group should be found"
assert gshadow_entry.name == "tgroup", "Incorrect groupname"
assert gshadow_entry.password == "!", "Incorrect password"
34 changes: 34 additions & 0 deletions tests/system/tests/test_newgrp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
Test newgrp
"""

from __future__ import annotations

import pytest

from framework.roles.shadow import Shadow
from framework.topology import KnownTopology


@pytest.mark.topology(KnownTopology.Shadow)
def test_newgrp__change_to_new_group(shadow: Shadow):
"""
:title: Change to a new group using newgrp
:setup:
1. Create test user and group
2. Add user to the group
:steps:
1. Use newgrp to change to the new group
2. Check that the group change worked
:expectedresults:
1. newgrp command succeeds
2. Current group ID matches the new group
:customerscenario: False
"""
shadow.useradd("tuser")
shadow.groupadd("tgroup")
shadow.usermod("-a -G tgroup tuser")

cmd, gid = shadow.newgrp("tgroup", run_as="tuser")
assert cmd.rc == 0, "newgrp command should succeed"
assert gid == 1001, f"Current GID should be {1001}, got {gid}"
Loading
Loading