Skip to content
Merged
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
30 changes: 29 additions & 1 deletion systemUserManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,35 @@ def getUserHome(username:str)->str|None:
except KeyError:
return None
return pw.pw_dir


@staticmethod
def getUserShell(username:str)->str|None:
"""Return the configured login shell for a system user."""
try:
return pwd.getpwnam(username).pw_shell or None
except KeyError:
return None

@staticmethod
def userUsesBash(username:str)->bool:
"""True when the user's configured login shell is Bash."""
shell = sshMng.getUserShell(username)
return bool(shell) and os.path.basename(shell) == "bash"

@staticmethod
def setUserShell(username:str, shell:str="/bin/bash")->Union[str, None]:
"""Set a system user's login shell. Returns an error string on failure."""
if not userExists(username):
return f"System user does not exist: {username}"
if not isinstance(shell, str) or not os.path.isabs(shell) or not os.path.isfile(shell):
return f"Invalid login shell: {shell}"
try:
subprocess.run(['usermod', '-s', shell, username], check=True)
except (OSError, subprocess.CalledProcessError) as e:
log.error(f"Error changing login shell for user {username} to {shell}: {e}", exc_info=True)
return f"Error changing login shell for user {username}: {e}"
return None

@staticmethod
def getFilePath_auth(username, check:bool=False) -> Union[str, None]:
"""
Expand Down
37 changes: 37 additions & 0 deletions tests/test_system_user_shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import os
import subprocess
import unittest
from types import SimpleNamespace
from unittest.mock import patch

from libs.JBLibs.systemUserManager import sshMng


class TestSystemUserLoginShell(unittest.TestCase):
@patch("libs.JBLibs.systemUserManager.pwd.getpwnam")
def test_get_user_shell(self, getpwnam):
getpwnam.return_value = SimpleNamespace(pw_shell="/bin/sh")
self.assertEqual(sshMng.getUserShell("alice"), "/bin/sh")
self.assertFalse(sshMng.userUsesBash("alice"))

getpwnam.return_value = SimpleNamespace(pw_shell="/usr/bin/bash")
self.assertTrue(sshMng.userUsesBash("alice"))

@patch("libs.JBLibs.systemUserManager.subprocess.run")
@patch("libs.JBLibs.systemUserManager.os.path.isfile", return_value=True)
@patch("libs.JBLibs.systemUserManager.userExists", return_value=True)
def test_set_user_shell_to_bash(self, user_exists, isfile, run):
self.assertIsNone(sshMng.setUserShell("alice"))
run.assert_called_once_with(["usermod", "-s", "/bin/bash", "alice"], check=True)

@patch("libs.JBLibs.systemUserManager.subprocess.run")
@patch("libs.JBLibs.systemUserManager.os.path.isfile", return_value=True)
@patch("libs.JBLibs.systemUserManager.userExists", return_value=True)
def test_set_user_shell_reports_usermod_failure(self, user_exists, isfile, run):
run.side_effect = subprocess.CalledProcessError(1, ["usermod"])
error = sshMng.setUserShell("alice")
self.assertIn("Error changing login shell", error)


if __name__ == "__main__":
unittest.main()