diff --git a/.fmf/version b/.fmf/version deleted file mode 100644 index d00491f..0000000 --- a/.fmf/version +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/.gitignore b/.gitignore index 3023995..ff11943 100644 --- a/.gitignore +++ b/.gitignore @@ -40,17 +40,3 @@ shadow-4.1.4.2.tar.bz2 /shadow-4.17.0.tar.xz.asc /shadow-4.17.4.tar.xz /shadow-4.17.4.tar.xz.asc -/shadow-4.18.0.tar.xz -/shadow-4.18.0.tar.xz.asc -/shadow-4.19.0.tar.xz -/shadow-4.19.0.tar.xz.asc -/shadow-4.19.2.tar.xz -/shadow-4.19.2.tar.xz.asc -/shadow-4.19.3.tar.xz -/shadow-4.19.3.tar.xz.asc -/shadow-4.20.0-rc2.tar.xz -/shadow-4.20.0-rc2.tar.xz.asc -/shadow-4.20.0-rc3.tar.xz -/shadow-4.20.0-rc3.tar.xz.asc -/shadow-4.20.0.tar.xz -/shadow-4.20.0.tar.xz.asc diff --git a/plans/tier0-functional.fmf b/plans/tier0-functional.fmf deleted file mode 100644 index a9939b5..0000000 --- a/plans/tier0-functional.fmf +++ /dev/null @@ -1,51 +0,0 @@ -summary: Tier 0 functional tests for shadow-utils -description: | - Run comprehensive system tests for shadow-utils. These tests validate user and group - account management functionality by testing actual system operations including user - creation, password management, group operations, and verification of system files - (i.e. /etc/passwd, /etc/shadow). - -provision: - how: virtual - image: fedora - -prepare: - - name: Install general dependencies - how: install - package: - - expect - - gcc - - git - - libssh-devel - - python3-devel - - python3-pip - - - name: Setup SSH keys for localhost testing - how: shell - script: - - ssh-keygen -t rsa -f /root/.ssh/id_rsa -N "" -q - - cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys - - chmod 600 /root/.ssh/authorized_keys - - ssh-keyscan -H localhost >> /root/.ssh/known_hosts - - - name: Clone shadow repository - how: shell - script: - - git clone https://github.com/shadow-maint/shadow.git /tmp/shadow-test - - - name: Copy test topology for Fedora CI - how: shell - script: - - cp tests/mhc-fedora-ci.yaml /tmp/shadow-test/tests/system - - - name: Install test dependencies - how: shell - script: - - pip3 install -r /tmp/shadow-test/tests/system/requirements.txt - -execute: - how: tmt - duration: 30m - script: | - cd /tmp/shadow-test/tests/system - pytest --mh-config=mhc-fedora-ci.yaml --mh-lazy-ssh -v diff --git a/shadow-4.13-newidmap-support-passing-pid-as-fd.patch b/shadow-4.13-newidmap-support-passing-pid-as-fd.patch new file mode 100644 index 0000000..37e0d41 --- /dev/null +++ b/shadow-4.13-newidmap-support-passing-pid-as-fd.patch @@ -0,0 +1,441 @@ +From 6974df39a708abf8bafbdfa2b7827e0f70f874cb Mon Sep 17 00:00:00 2001 +From: Serge Hallyn +Date: Mon, 6 Feb 2023 22:49:42 -0600 +Subject: [PATCH] newuidmap and newgidmap: support passing pid as fd + +Closes #635 + +newuidmap and newgidmap currently take an integner pid as +the first argument, determining the process id on which to +act. Accept also "fd:N", where N must be an open file +descriptor to the /proc/pid directory for the process to +act upon. This way, if you + +exec 10 +--- + lib/get_pid.c | 51 +++++++++++++++++++++++++++++++++++++++++++++ + lib/prototypes.h | 2 ++ + man/newgidmap.1.xml | 11 ++++++++++ + man/newuidmap.1.xml | 11 ++++++++++ + src/newgidmap.c | 41 ++++++++++++++---------------------- + src/newuidmap.c | 40 +++++++++++++---------------------- + 6 files changed, 106 insertions(+), 50 deletions(-) + +diff --git a/lib/get_pid.c b/lib/get_pid.c +index 10184bf0..ab91d158 100644 +--- a/lib/get_pid.c ++++ b/lib/get_pid.c +@@ -10,6 +10,9 @@ + + #include "prototypes.h" + #include "defines.h" ++#include ++#include ++#include + + int get_pid (const char *pidstr, pid_t *pid) + { +@@ -29,3 +32,51 @@ int get_pid (const char *pidstr, pid_t *pid) + return 1; + } + ++/* ++ * If use passed in fd:4 as an argument, then return the ++ * value '4', the fd to use. ++ */ ++int get_pidfd_from_fd(const char *pidfdstr) ++{ ++ long long int val; ++ char *endptr; ++ ++ errno = 0; ++ val = strtoll (pidfdstr, &endptr, 10); ++ if ( ('\0' == *pidfdstr) ++ || ('\0' != *endptr) ++ || (ERANGE == errno) ++ || (/*@+longintegral@*/val != (pid_t)val)/*@=longintegral@*/) { ++ return 0; ++ } ++ ++ return (int)val; ++} ++ ++int open_pidfd(const char *pidstr) ++{ ++ int proc_dir_fd; ++ int written; ++ char proc_dir_name[32]; ++ pid_t target; ++ ++ if (get_pid(pidstr, &target) == 0) ++ return -ENOENT; ++ ++ /* max string length is 6 + 10 + 1 + 1 = 18, allocate 32 bytes */ ++ written = snprintf(proc_dir_name, sizeof(proc_dir_name), "/proc/%u/", ++ target); ++ if ((written <= 0) || ((size_t)written >= sizeof(proc_dir_name))) { ++ fprintf(stderr, "snprintf of proc path failed for %u: %s\n", ++ target, strerror(errno)); ++ return -EINVAL; ++ } ++ ++ proc_dir_fd = open(proc_dir_name, O_DIRECTORY); ++ if (proc_dir_fd < 0) { ++ fprintf(stderr, _("Could not open proc directory for target %u: %s\n"), ++ target, strerror(errno)); ++ return -EINVAL; ++ } ++ return proc_dir_fd; ++} +diff --git a/lib/prototypes.h b/lib/prototypes.h +index 400d5b97..21df6f61 100644 +--- a/lib/prototypes.h ++++ b/lib/prototypes.h +@@ -160,6 +160,8 @@ extern int getlong (const char *numstr, /*@out@*/long int *result); + + /* get_pid.c */ + extern int get_pid (const char *pidstr, pid_t *pid); ++extern int get_pidfd_from_fd(const char *pidfdstr); ++extern int open_pidfd(const char *pidstr); + + /* getrange */ + extern int getrange (const char *range, +diff --git a/man/newgidmap.1.xml b/man/newgidmap.1.xml +index e4ebc69e..9b7683eb 100644 +--- a/man/newgidmap.1.xml ++++ b/man/newgidmap.1.xml +@@ -116,6 +116,17 @@ + + Note that newgidmap may be used only once for a given process. + ++ ++ Instead of an integer process id, the first argument may be ++ specified as fd:N, where the integer N ++ is the file descriptor number for the calling process's opened ++ file for /proc/[pid[. In this case, ++ newgidmap will use ++ openat2 ++ to open the gid_map file under that ++ directory, avoiding a TOCTTOU in case the process exits and ++ the pid is immediately reused. ++ + + + +diff --git a/man/newuidmap.1.xml b/man/newuidmap.1.xml +index f5cb5b48..ca917a77 100644 +--- a/man/newuidmap.1.xml ++++ b/man/newuidmap.1.xml +@@ -116,6 +116,17 @@ + + Note that newuidmap may be used only once for a given process. + ++ ++ Instead of an integer process id, the first argument may be ++ specified as fd:N, where the integer N ++ is the file descriptor number for the calling process's opened ++ file for /proc/[pid[. In this case, ++ newuidmap will use ++ openat2 ++ to open the uid_map file under that ++ directory, avoiding a TOCTTOU in case the process exits and ++ the pid is immediately reused. ++ + + + +diff --git a/src/newgidmap.c b/src/newgidmap.c +index 01d0fe90..d6d29725 100644 +--- a/src/newgidmap.c ++++ b/src/newgidmap.c +@@ -69,7 +69,7 @@ static void verify_ranges(struct passwd *pw, int ranges, + + static void usage(void) + { +- fprintf(stderr, _("usage: %s [ ] ... \n"), Prog); ++ fprintf(stderr, _("usage: %s [] [ ] ... \n"), Prog); + exit(EXIT_FAILURE); + } + +@@ -143,15 +143,12 @@ out: + */ + int main(int argc, char **argv) + { +- char proc_dir_name[32]; + char *target_str; +- pid_t target; + int proc_dir_fd; + int ranges; + struct map_range *mappings; + struct stat st; + struct passwd *pw; +- int written; + bool allow_setgroups = false; + + Prog = Basename (argv[0]); +@@ -168,25 +165,19 @@ int main(int argc, char **argv) + /* Find the process that needs its user namespace + * gid mapping set. + */ +- target_str = argv[1]; +- if (!get_pid(target_str, &target)) +- usage(); + +- /* max string length is 6 + 10 + 1 + 1 = 18, allocate 32 bytes */ +- written = snprintf(proc_dir_name, sizeof(proc_dir_name), "/proc/%u/", +- target); +- if ((written <= 0) || (written >= sizeof(proc_dir_name))) { +- fprintf(stderr, "%s: snprintf of proc path failed: %s\n", +- Prog, strerror(errno)); +- } +- +- proc_dir_fd = open(proc_dir_name, O_DIRECTORY); +- if (proc_dir_fd < 0) { +- fprintf(stderr, _("%s: Could not open proc directory for target %u\n"), +- Prog, target); +- return EXIT_FAILURE; ++ target_str = argv[1]; ++ if (strlen(target_str) > 3 && strncmp(target_str, "fd:", 3) == 0) { ++ /* the user passed in a /proc/pid fd for the process */ ++ target_str = &target_str[3]; ++ proc_dir_fd = get_pidfd_from_fd(target_str); ++ if (proc_dir_fd < 0) ++ usage(); ++ } else { ++ proc_dir_fd = open_pidfd(target_str); ++ if (proc_dir_fd < 0) ++ usage(); + } +- + /* Who am i? */ + pw = get_my_pwent (); + if (NULL == pw) { +@@ -200,8 +191,8 @@ int main(int argc, char **argv) + + /* Get the effective uid and effective gid of the target process */ + if (fstat(proc_dir_fd, &st) < 0) { +- fprintf(stderr, _("%s: Could not stat directory for target %u\n"), +- Prog, target); ++ fprintf(stderr, _("%s: Could not stat directory for process\n"), ++ Prog); + return EXIT_FAILURE; + } + +@@ -213,8 +204,8 @@ int main(int argc, char **argv) + (!getdef_bool("GRANT_AUX_GROUP_SUBIDS") && (getgid() != pw->pw_gid)) || + (pw->pw_uid != st.st_uid) || + (getgid() != st.st_gid)) { +- fprintf(stderr, _( "%s: Target %u is owned by a different user: uid:%lu pw_uid:%lu st_uid:%lu, gid:%lu pw_gid:%lu st_gid:%lu\n" ), +- Prog, target, ++ fprintf(stderr, _( "%s: Target process is owned by a different user: uid:%lu pw_uid:%lu st_uid:%lu, gid:%lu pw_gid:%lu st_gid:%lu\n" ), ++ Prog, + (unsigned long int)getuid(), (unsigned long int)pw->pw_uid, (unsigned long int)st.st_uid, + (unsigned long int)getgid(), (unsigned long int)pw->pw_gid, (unsigned long int)st.st_gid); + return EXIT_FAILURE; +diff --git a/src/newuidmap.c b/src/newuidmap.c +index e8798409..e99655c9 100644 +--- a/src/newuidmap.c ++++ b/src/newuidmap.c +@@ -64,7 +64,7 @@ static void verify_ranges(struct passwd *pw, int ranges, + + static void usage(void) + { +- fprintf(stderr, _("usage: %s [ ] ... \n"), Prog); ++ fprintf(stderr, _("usage: %s [|fd:] [ ] ... \n"), Prog); + exit(EXIT_FAILURE); + } + +@@ -73,15 +73,12 @@ static void usage(void) + */ + int main(int argc, char **argv) + { +- char proc_dir_name[32]; + char *target_str; +- pid_t target; + int proc_dir_fd; + int ranges; + struct map_range *mappings; + struct stat st; + struct passwd *pw; +- int written; + + Prog = Basename (argv[0]); + log_set_progname(Prog); +@@ -94,26 +91,20 @@ int main(int argc, char **argv) + if (argc < 2) + usage(); + ++ target_str = argv[1]; + /* Find the process that needs its user namespace + * uid mapping set. + */ +- target_str = argv[1]; +- if (!get_pid(target_str, &target)) +- usage(); +- +- /* max string length is 6 + 10 + 1 + 1 = 18, allocate 32 bytes */ +- written = snprintf(proc_dir_name, sizeof(proc_dir_name), "/proc/%u/", +- target); +- if ((written <= 0) || (written >= sizeof(proc_dir_name))) { +- fprintf(stderr, "%s: snprintf of proc path failed: %s\n", +- Prog, strerror(errno)); +- } +- +- proc_dir_fd = open(proc_dir_name, O_DIRECTORY); +- if (proc_dir_fd < 0) { +- fprintf(stderr, _("%s: Could not open proc directory for target %u\n"), +- Prog, target); +- return EXIT_FAILURE; ++ if (strlen(target_str) > 3 && strncmp(target_str, "fd:", 3) == 0) { ++ /* the user passed in a /proc/pid fd for the process */ ++ target_str = &target_str[3]; ++ proc_dir_fd = get_pidfd_from_fd(target_str); ++ if (proc_dir_fd < 0) ++ usage(); ++ } else { ++ proc_dir_fd = open_pidfd(target_str); ++ if (proc_dir_fd < 0) ++ usage(); + } + + /* Who am i? */ +@@ -129,8 +120,7 @@ int main(int argc, char **argv) + + /* Get the effective uid and effective gid of the target process */ + if (fstat(proc_dir_fd, &st) < 0) { +- fprintf(stderr, _("%s: Could not stat directory for target %u\n"), +- Prog, target); ++ fprintf(stderr, _("%s: Could not stat directory for target process\n"), Prog); + return EXIT_FAILURE; + } + +@@ -142,8 +132,8 @@ int main(int argc, char **argv) + (!getdef_bool("GRANT_AUX_GROUP_SUBIDS") && (getgid() != pw->pw_gid)) || + (pw->pw_uid != st.st_uid) || + (getgid() != st.st_gid)) { +- fprintf(stderr, _( "%s: Target process %u is owned by a different user: uid:%lu pw_uid:%lu st_uid:%lu, gid:%lu pw_gid:%lu st_gid:%lu\n" ), +- Prog, target, ++ fprintf(stderr, _( "%s: Target process is owned by a different user: uid:%lu pw_uid:%lu st_uid:%lu, gid:%lu pw_gid:%lu st_gid:%lu\n" ), ++ Prog, + (unsigned long int)getuid(), (unsigned long int)pw->pw_uid, (unsigned long int)st.st_uid, + (unsigned long int)getgid(), (unsigned long int)pw->pw_gid, (unsigned long int)st.st_gid); + return EXIT_FAILURE; +-- +2.39.2 + +From 7ff33fae6f9cd79c0e012671c37a172e9a681d0b Mon Sep 17 00:00:00 2001 +From: Serge Hallyn +Date: Fri, 24 Feb 2023 13:52:32 -0600 +Subject: [PATCH] get_pidfd_from_fd: return -1 on error, not 0 + +Fixes: 6974df39a: newuidmap and newgidmap: support passing pid as fd +Signed-off-by: Serge Hallyn +--- + lib/get_pid.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/lib/get_pid.c b/lib/get_pid.c +index ab91d158..5b6d9da4 100644 +--- a/lib/get_pid.c ++++ b/lib/get_pid.c +@@ -35,6 +35,7 @@ int get_pid (const char *pidstr, pid_t *pid) + /* + * If use passed in fd:4 as an argument, then return the + * value '4', the fd to use. ++ * On error, return -1. + */ + int get_pidfd_from_fd(const char *pidfdstr) + { +@@ -47,7 +48,7 @@ int get_pidfd_from_fd(const char *pidfdstr) + || ('\0' != *endptr) + || (ERANGE == errno) + || (/*@+longintegral@*/val != (pid_t)val)/*@=longintegral@*/) { +- return 0; ++ return -1; + } + + return (int)val; +-- +2.39.2 + +From 05e2adf509ba0e3779dae66a276b86927a8e1e0e Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Vin=C3=ADcius=20dos=20Santos=20Oliveira?= + +Date: Fri, 24 Feb 2023 18:06:02 -0300 +Subject: [PATCH] Validate fds created by the user + +write_mapping() will do the following: + +openat(proc_dir_fd, map_file, O_WRONLY); + +An attacker could create a directory containing a symlink named +"uid_map" pointing to any file owned by root, and thus allow him to +overwrite any root-owned file. +--- + lib/get_pid.c | 17 +++++++++++++++++ + 1 file changed, 17 insertions(+) + +diff --git a/lib/get_pid.c b/lib/get_pid.c +index 5b6d9da4..8e5e6014 100644 +--- a/lib/get_pid.c ++++ b/lib/get_pid.c +@@ -41,6 +41,8 @@ int get_pidfd_from_fd(const char *pidfdstr) + { + long long int val; + char *endptr; ++ struct stat st; ++ dev_t proc_st_dev, proc_st_rdev; + + errno = 0; + val = strtoll (pidfdstr, &endptr, 10); +@@ -51,6 +53,21 @@ int get_pidfd_from_fd(const char *pidfdstr) + return -1; + } + ++ if (stat("/proc/self/uid_map", &st) < 0) { ++ return -1; ++ } ++ ++ proc_st_dev = st.st_dev; ++ proc_st_rdev = st.st_rdev; ++ ++ if (fstat(val, &st) < 0) { ++ return -1; ++ } ++ ++ if (st.st_dev != proc_st_dev || st.st_rdev != proc_st_rdev) { ++ return -1; ++ } ++ + return (int)val; + } + +-- +2.39.2 + diff --git a/shadow-4.15.0-manfix.patch b/shadow-4.15.0-manfix.patch new file mode 100644 index 0000000..34e62f9 --- /dev/null +++ b/shadow-4.15.0-manfix.patch @@ -0,0 +1,162 @@ +diff -up shadow-4.15.0/man/groupmems.8.xml.manfix shadow-4.15.0/man/groupmems.8.xml +--- shadow-4.15.0/man/groupmems.8.xml.manfix 2023-05-26 04:56:11.000000000 +0200 ++++ shadow-4.15.0/man/groupmems.8.xml 2024-02-09 10:42:20.337036378 +0100 +@@ -156,20 +156,10 @@ + + SETUP + +- The groupmems executable should be in mode +- 2710 as user root and in group +- groups. The system administrator can add users to +- group groups to allow or disallow them using the +- groupmems utility to manage their own group +- membership list. ++ In this operating system the groupmems executable ++ is not setuid and regular users cannot use it to manipulate ++ the membership of their own group. + +- +- +- $ groupadd -r groups +- $ chmod 2710 groupmems +- $ chown root:groups groupmems +- $ groupmems -g groups -a gk4 +- + + + +diff -up shadow-4.15.0/man/ja/man5/login.defs.5.manfix shadow-4.15.0/man/ja/man5/login.defs.5 +--- shadow-4.15.0/man/ja/man5/login.defs.5.manfix 2023-03-13 21:58:56.000000000 +0100 ++++ shadow-4.15.0/man/ja/man5/login.defs.5 2024-02-09 10:42:20.337036378 +0100 +@@ -123,10 +123,6 @@ 以下の参照表は、 + shadow パスワード機能のどのプログラムが + どのパラメータを使用するかを示したものである。 + .na +-.IP chfn 12 +-CHFN_AUTH CHFN_RESTRICT +-.IP chsh 12 +-CHFN_AUTH + .IP groupadd 12 + GID_MAX GID_MIN + .IP newusers 12 +diff -up shadow-4.15.0/man/login.defs.5.xml.manfix shadow-4.15.0/man/login.defs.5.xml +--- shadow-4.15.0/man/login.defs.5.xml.manfix 2024-01-22 22:36:43.000000000 +0100 ++++ shadow-4.15.0/man/login.defs.5.xml 2024-02-09 10:45:49.014407259 +0100 +@@ -144,6 +144,17 @@ + long numeric parameters is machine-dependent. + + ++ ++ Please note that the parameters in this configuration file control the ++ behavior of the tools from the shadow-utils component. None of these ++ tools uses the PAM mechanism, and the utilities that use PAM (such as the ++ passwd command) should be configured elsewhere. The only values that ++ affect PAM modules are ENCRYPT_METHOD and SHA_CRYPT_MAX_ROUNDS ++ for pam_unix module, FAIL_DELAY for pam_faildelay module, ++ and UMASK for pam_umask module. Refer to ++ pam(8) for more information. ++ ++ + The following configuration items are provided: + + +@@ -240,16 +251,6 @@ + + + +- chfn +- +- +- CHFN_AUTH +- CHFN_RESTRICT +- LOGIN_STRING +- +- +- +- + chgpasswd + + +@@ -276,14 +277,6 @@ + + + +- +- chsh +- +- +- CHSH_AUTH LOGIN_STRING +- +- +- + + + +@@ -352,34 +345,6 @@ + LASTLOG_UID_MAX + + +- +- login +- +- +- CONSOLE +- CONSOLE_GROUPS DEFAULT_HOME +- ENV_HZ ENV_PATH ENV_SUPATH +- ENV_TZ ENVIRON_FILE +- ERASECHAR FAIL_DELAY +- FAILLOG_ENAB +- FAKE_SHELL +- FTMP_FILE +- HUSHLOGIN_FILE +- ISSUE_FILE +- KILLCHAR +- LASTLOG_ENAB LASTLOG_UID_MAX +- LOGIN_RETRIES +- LOGIN_STRING +- LOGIN_TIMEOUT LOG_OK_LOGINS LOG_UNKFAIL_ENAB +- MAIL_CHECK_ENAB MAIL_DIR MAIL_FILE +- MOTD_FILE NOLOGINS_FILE PORTTIME_CHECKS_ENAB +- QUOTAS_ENAB +- TTYGROUP TTYPERM TTYTYPE_FILE +- ULIMIT UMASK +- USERGROUPS_ENAB +- +- +- + + + newgrp / sg +@@ -451,32 +416,6 @@ + + + +- +- su +- +- +- CONSOLE +- CONSOLE_GROUPS DEFAULT_HOME +- ENV_HZ ENVIRON_FILE +- ENV_PATH ENV_SUPATH +- ENV_TZ LOGIN_STRING MAIL_CHECK_ENAB +- MAIL_DIR MAIL_FILE QUOTAS_ENAB +- SULOG_FILE SU_NAME +- SU_WHEEL_ONLY +- SYSLOG_SU_ENAB +- USERGROUPS_ENAB +- +- +- +- +- sulogin +- +- +- ENV_HZ +- ENV_TZ +- +- +- + + useradd + diff --git a/shadow-4.17.4-account-tools-setuid.patch b/shadow-4.17.4-account-tools-setuid.patch new file mode 100644 index 0000000..4c1ff92 --- /dev/null +++ b/shadow-4.17.4-account-tools-setuid.patch @@ -0,0 +1,380 @@ +diff -up shadow-4.15.0/src/chpasswd.c.account-tools-setuid shadow-4.15.0/src/chpasswd.c +--- shadow-4.15.0/src/chpasswd.c.account-tools-setuid 2024-03-08 22:27:04.000000000 +0100 ++++ shadow-4.15.0/src/chpasswd.c 2024-03-11 11:21:57.561150382 +0100 +@@ -443,9 +443,11 @@ int main (int argc, char **argv) + char *cp; + const char *salt; + ++#ifdef ACCT_TOOLS_SETUID + #ifdef USE_PAM + bool use_pam = true; + #endif /* USE_PAM */ ++#endif /* ACCT_TOOLS_SETUID */ + + bool errors = false; + intmax_t line = 0; +@@ -469,19 +471,23 @@ int main (int argc, char **argv) + process_root_flag ("-R", argc, argv); + prefix = process_prefix_flag ("-P", argc, argv); + ++#ifdef ACCT_TOOLS_SETUID + #ifdef USE_PAM + if (md5flg || eflg || cflg || prefix[0]) { + use_pam = false; + } + #endif /* USE_PAM */ ++#endif /* ACCT_TOOLS_SETUID */ + + OPENLOG (Prog); + + check_perms (); + ++#ifdef ACCT_TOOLS_SETUID + #ifdef USE_PAM + if (!use_pam) + #endif /* USE_PAM */ ++#endif /* ACCT_TOOLS_SETUID */ + { + is_shadow_pwd = spw_file_present (); + +@@ -543,6 +549,7 @@ int main (int argc, char **argv) + } + newpwd = cp; + ++#ifdef ACCT_TOOLS_SETUID + #ifdef USE_PAM + if (use_pam) { + if (do_pam_passwd_non_interactive (Prog, name, newpwd) != 0) { +@@ -553,6 +560,7 @@ int main (int argc, char **argv) + } + } else + #endif /* USE_PAM */ ++#endif /* ACCT_TOOLS_SETUID */ + { + const struct spwd *sp; + struct spwd newsp; +@@ -672,9 +680,11 @@ int main (int argc, char **argv) + * password database. + */ + if (errors) { ++#ifdef ACCT_TOOLS_SETUID + #ifdef USE_PAM + if (!use_pam) + #endif /* USE_PAM */ ++#endif /* ACCT_TOOLS_SETUID */ + { + fprintf (stderr, + _("%s: error detected, changes ignored\n"), +@@ -683,9 +693,11 @@ int main (int argc, char **argv) + fail_exit (1); + } + ++#ifdef ACCT_TOOLS_SETUID + #ifdef USE_PAM + if (!use_pam) + #endif /* USE_PAM */ ++#endif /* ACCT_TOOLS_SETUID */ + { + /* Save the changes */ + close_files (); +diff -up shadow-4.15.0/src/groupmems.c.account-tools-setuid shadow-4.15.0/src/groupmems.c +--- shadow-4.15.0/src/groupmems.c.account-tools-setuid 2024-03-08 22:27:04.000000000 +0100 ++++ shadow-4.15.0/src/groupmems.c 2024-03-11 11:16:18.365408572 +0100 +@@ -14,9 +14,11 @@ + #include + #include + #include ++#ifdef ACCT_TOOLS_SETUID + #ifdef USE_PAM + #include "pam_defs.h" + #endif /* USE_PAM */ ++#endif /* ACCT_TOOLS_SETUID */ + #include + + #include "alloc/x/xmalloc.h" +@@ -430,6 +432,7 @@ static void process_flags (int argc, cha + static void check_perms (void) + { + if (!list) { ++#ifdef ACCT_TOOLS_SETUID + #ifdef USE_PAM + pam_handle_t *pamh = NULL; + int retval; +@@ -463,7 +466,8 @@ static void check_perms (void) + fail_exit (1); + } + (void) pam_end (pamh, retval); +-#endif ++#endif /* USE_PAM */ ++#endif /* ACCT_TOOLS_SETUID */ + } + } + +diff -up shadow-4.15.0/src/newusers.c.account-tools-setuid shadow-4.15.0/src/newusers.c +--- shadow-4.15.0/src/newusers.c.account-tools-setuid 2024-03-08 22:27:04.000000000 +0100 ++++ shadow-4.15.0/src/newusers.c 2024-03-11 11:20:07.198909046 +0100 +@@ -59,6 +59,7 @@ + static const char Prog[] = "newusers"; + + static bool rflg = false; /* create a system account */ ++#ifndef ACCT_TOOLS_SETUID + #ifndef USE_PAM + static /*@null@*//*@observer@*/char *crypt_method = NULL; + #define cflg (NULL != crypt_method) +@@ -75,6 +76,7 @@ static long bcrypt_rounds = 13; + static long yescrypt_cost = 5; + #endif /* USE_YESCRYPT */ + #endif /* !USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID */ + + static bool is_shadow; + #ifdef SHADOWGRP +@@ -97,9 +99,11 @@ NORETURN static void fail_exit (int); + static int add_group (const char *, const char *, gid_t *, gid_t); + static int get_user_id (const char *, uid_t *); + static int add_user (const char *, uid_t, gid_t); ++#ifndef ACCT_TOOLS_SETUID + #ifndef USE_PAM + static int update_passwd (struct passwd *, const char *); + #endif /* !USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID */ + static int add_passwd (struct passwd *, const char *); + static void process_flags (int argc, char **argv); + static void check_flags (void); +@@ -121,6 +125,7 @@ static void usage (int status) + "Options:\n"), + Prog); + (void) fputs (_(" -b, --badname allow bad names\n"), usageout); ++#ifndef ACCT_TOOLS_SETUID + #ifndef USE_PAM + (void) fprintf (usageout, + _(" -c, --crypt-method METHOD the crypt method (one of %s)\n"), +@@ -136,9 +141,11 @@ static void usage (int status) + #endif + ); + #endif /* !USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID */ + (void) fputs (_(" -h, --help display this help message and exit\n"), usageout); + (void) fputs (_(" -r, --system create system accounts\n"), usageout); + (void) fputs (_(" -R, --root CHROOT_DIR directory to chroot into\n"), usageout); ++#ifndef ACCT_TOOLS_SETUID + #ifndef USE_PAM + #if defined(USE_SHA_CRYPT) || defined(USE_BCRYPT) || defined(USE_YESCRYPT) + (void) fputs (_(" -s, --sha-rounds number of rounds for the SHA, BCRYPT\n" +@@ -146,6 +153,7 @@ static void usage (int status) + usageout); + #endif /* USE_SHA_CRYPT || USE_BCRYPT || USE_YESCRYPT */ + #endif /* !USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID */ + (void) fputs ("\n", usageout); + + exit (status); +@@ -405,6 +413,7 @@ static int add_user (const char *name, u + return (pw_update (&pwent) == 0) ? -1 : 0; + } + ++#ifndef ACCT_TOOLS_SETUID + #ifndef USE_PAM + /* + * update_passwd - update the password in the passwd entry +@@ -457,6 +466,7 @@ static int update_passwd (struct passwd + return 0; + } + #endif /* !USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID */ + + /* + * add_passwd - add or update the encrypted password +@@ -465,10 +475,13 @@ static int add_passwd (struct passwd *pw + { + const struct spwd *sp; + struct spwd spent; ++#ifndef ACCT_TOOLS_SETUID + #ifndef USE_PAM + char *cp; + #endif /* !USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID */ + ++#ifndef ACCT_TOOLS_SETUID + #ifndef USE_PAM + void *crypt_arg = NULL; + if (NULL != crypt_method) { +@@ -505,13 +518,14 @@ static int add_passwd (struct passwd *pw + return update_passwd (pwd, password); + } + #endif /* USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID */ + + /* + * Do the first and easiest shadow file case. The user already + * exists in the shadow password file. + */ + sp = spw_locate (pwd->pw_name); +-#ifndef USE_PAM ++#if !defined(ACCT_TOOLS_SETUID) && !defined(USE_PAM) + if (NULL != sp) { + spent = *sp; + if ( (NULL != crypt_method) +@@ -547,7 +561,7 @@ static int add_passwd (struct passwd *pw + if (!streq(pwd->pw_passwd, "x")) { + return update_passwd (pwd, password); + } +-#else /* USE_PAM */ ++#else /* !ACCT_TOOLS_SETUID && !USE_PAM */ + /* + * If there is already a shadow entry, do not touch it. + * If there is already a passwd entry with a password, do not +@@ -558,14 +572,14 @@ static int add_passwd (struct passwd *pw + || !streq(pwd->pw_passwd, "x")) { + return 0; + } +-#endif /* USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID && !USE_PAM */ + + /* + * Now the really hard case - I need to create an entirely new + * shadow password file entry. + */ + spent.sp_namp = pwd->pw_name; +-#ifndef USE_PAM ++#if !defined(ACCT_TOOLS_SETUID) && !defined(USE_PAM) + if ((crypt_method != NULL) && streq(crypt_method, "NONE")) { + spent.sp_pwdp = (char *)password; + } else { +@@ -610,35 +624,41 @@ static int add_passwd (struct passwd *pw + static void process_flags (int argc, char **argv) + { + int c; ++#ifndef ACCT_TOOLS_SETUID + #ifndef USE_PAM + #if defined(USE_SHA_CRYPT) || defined(USE_BCRYPT) || defined(USE_YESCRYPT) + int bad_s; + #endif /* USE_SHA_CRYPT || USE_BCRYPT || USE_YESCRYPT */ + #endif /* !USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID */ + static struct option long_options[] = { + {"badname", no_argument, NULL, 'b'}, ++#ifndef ACCT_TOOLS_SETUID + #ifndef USE_PAM + {"crypt-method", required_argument, NULL, 'c'}, + #endif /* !USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID */ + {"help", no_argument, NULL, 'h'}, + {"system", no_argument, NULL, 'r'}, + {"root", required_argument, NULL, 'R'}, ++#ifndef ACCT_TOOLS_SETUID + #ifndef USE_PAM + #if defined(USE_SHA_CRYPT) || defined(USE_BCRYPT) || defined(USE_YESCRYPT) + {"sha-rounds", required_argument, NULL, 's'}, + #endif /* USE_SHA_CRYPT || USE_BCRYPT || USE_YESCRYPT */ + #endif /* !USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID */ + {NULL, 0, NULL, '\0'} + }; + + while ((c = getopt_long (argc, argv, +-#ifndef USE_PAM ++#if !defined(ACCT_TOOLS_SETUID) && !defined(USE_PAM) + #if defined(USE_SHA_CRYPT) || defined(USE_BCRYPT) || defined(USE_YESCRYPT) + "c:bhrs:", + #else /* !USE_SHA_CRYPT && !USE_BCRYPT && !USE_YESCRYPT */ + "c:bhr", + #endif /* USE_SHA_CRYPT || USE_BCRYPT || USE_YESCRYPT */ +-#else /* USE_PAM */ ++#else /* !ACCT_TOOLS_SETUID && !USE_PAM */ + "bhr", + #endif + long_options, NULL)) != -1) { +@@ -646,11 +666,13 @@ static void process_flags (int argc, cha + case 'b': + allow_bad_names = true; + break; ++#ifndef ACCT_TOOLS_SETUID + #ifndef USE_PAM + case 'c': + crypt_method = optarg; + break; + #endif /* !USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID */ + case 'h': + usage (EXIT_SUCCESS); + break; +@@ -659,6 +681,7 @@ static void process_flags (int argc, cha + break; + case 'R': /* no-op, handled in process_root_flag () */ + break; ++#ifndef ACCT_TOOLS_SETUID + #ifndef USE_PAM + #if defined(USE_SHA_CRYPT) || defined(USE_BCRYPT) || defined(USE_YESCRYPT) + case 's': +@@ -698,6 +721,7 @@ static void process_flags (int argc, cha + break; + #endif /* USE_SHA_CRYPT || USE_BCRYPT || USE_YESCRYPT */ + #endif /* !USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID */ + default: + usage (EXIT_FAILURE); + break; +@@ -730,6 +754,7 @@ static void process_flags (int argc, cha + */ + static void check_flags (void) + { ++#ifndef ACCT_TOOLS_SETUID + #ifndef USE_PAM + #if defined(USE_SHA_CRYPT) || defined(USE_BCRYPT) || defined(USE_YESCRYPT) + if (sflg && !cflg) { +@@ -762,6 +787,7 @@ static void check_flags (void) + } + } + #endif /* !USE_PAM */ ++#endif /* !ACCT_TOOLS_SETUID */ + } + + /* +@@ -1052,12 +1078,14 @@ int main (int argc, char **argv) + intmax_t line = 0; + uid_t uid; + gid_t gid; ++#ifdef ACCT_TOOLS_SETUID + #ifdef USE_PAM + intmax_t *lines = NULL; + char **usernames = NULL; + char **passwords = NULL; + size_t nusers = 0; + #endif /* USE_PAM */ ++#endif /* ACCT_TOOLS_SETUID */ + + log_set_progname(Prog); + log_set_logfd(stderr); +@@ -1195,6 +1223,7 @@ int main (int argc, char **argv) + } + newpw = *pw; + ++#ifdef ACCT_TOOLS_SETUID + #ifdef USE_PAM + /* keep the list of user/password for later update by PAM */ + nusers++; +@@ -1211,6 +1240,7 @@ int main (int argc, char **argv) + usernames[nusers-1] = xstrdup(fields[0]); + passwords[nusers-1] = xstrdup(fields[1]); + #endif /* USE_PAM */ ++#endif /* ACCT_TOOLS_SETUID */ + if (add_passwd (&newpw, fields[1]) != 0) { + fprintf (stderr, + _("%s: line %jd: can't update password\n"), +@@ -1327,6 +1357,7 @@ int main (int argc, char **argv) + nscd_flush_cache ("group"); + sssd_flush_cache (SSSD_DB_PASSWD | SSSD_DB_GROUP); + ++#ifdef ACCT_TOOLS_SETUID + #ifdef USE_PAM + /* Now update the passwords using PAM */ + for (size_t i = 0; i < nusers; i++) { +@@ -1339,6 +1370,7 @@ int main (int argc, char **argv) + } + } + #endif /* USE_PAM */ ++#endif /* ACCT_TOOLS_SETUID */ + + exit (EXIT_SUCCESS); + } diff --git a/shadow-utils-configure-gshadow.patch b/shadow-utils-configure-gshadow.patch new file mode 100644 index 0000000..a983ce0 --- /dev/null +++ b/shadow-utils-configure-gshadow.patch @@ -0,0 +1,20 @@ +The missing #include causes the configure check to fail +spuriously, resulting in HAVE_SHADOWGRP not being defined. + +Submitted upstream: + +diff --git a/configure.ac b/configure.ac +index 924254a0c8171802..6c7d9839979e037d 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -116,6 +116,10 @@ if test "$ac_cv_header_shadow_h" = "yes"; then + ac_cv_libc_shadowgrp, + AC_RUN_IFELSE([AC_LANG_SOURCE([ + #include ++ #ifdef HAVE_GSHADOW_H ++ #include ++ #endif ++ int + main() + { + struct sgrp *sg = sgetsgent("test:x::"); diff --git a/shadow-utils.login.defs b/shadow-utils.login.defs index e84c7ab..1e9129a 100644 --- a/shadow-utils.login.defs +++ b/shadow-utils.login.defs @@ -148,7 +148,7 @@ SYS_UID_MAX 999 # Extra per user uids SUB_UID_MIN 524288 SUB_UID_MAX 600100000 -SUB_UID_COUNT 65536 +SUB_UID_COUNT 0 # # Min/max values for automatic gid selection in groupadd(8) @@ -161,7 +161,7 @@ SYS_GID_MAX 999 # Extra per user group ids SUB_GID_MIN 524288 SUB_GID_MAX 600100000 -SUB_GID_COUNT 65536 +SUB_GID_COUNT 0 # # Max number of login(1) retries if password is bad diff --git a/shadow-utils.spec b/shadow-utils.spec index 7bc7e8b..b853fee 100644 --- a/shadow-utils.spec +++ b/shadow-utils.spec @@ -1,12 +1,12 @@ Summary: Utilities for managing accounts and shadow password files Name: shadow-utils -Version: 4.20.0 -Release: 1%{?dist} +Version: 4.17.4 +Release: 2%{?dist} Epoch: 2 License: BSD-3-Clause AND GPL-2.0-or-later URL: https://github.com/shadow-maint/shadow -Source0: https://github.com/shadow-maint/shadow/releases/download/4.20.0/shadow-4.20.0.tar.xz -Source1: https://github.com/shadow-maint/shadow/releases/download/4.20.0/shadow-4.20.0.tar.xz.asc +Source0: https://github.com/shadow-maint/shadow/releases/download/4.17.4/shadow-4.17.4.tar.xz +Source1: https://github.com/shadow-maint/shadow/releases/download/4.17.4/shadow-4.17.4.tar.xz.asc Source2: shadow-utils.useradd Source3: shadow-utils.login.defs Source4: shadow-bsd.txt @@ -18,8 +18,16 @@ Source7: passwd.pamd %global includesubiddir %{_includedir}/shadow # Fail linking if there are undefined symbols. %global _ld_strict_symbol_defs 1 +# workaround: _pam_confdir definition disappeared +%if ! %{defined _pam_confdir} +%define _pam_confdir /etc/pam.d +%endif ### Patches ### +# Misc manual page changes - non-upstreamable +Patch0: shadow-4.15.0-manfix.patch +# Probably non-upstreamable +Patch1: shadow-4.17.4-account-tools-setuid.patch ### Dependencies ### Requires: audit-libs >= 1.6.5 @@ -41,7 +49,6 @@ BuildRequires: git BuildRequires: itstool BuildRequires: libacl-devel BuildRequires: libattr-devel -BuildRequires: libcmocka-devel BuildRequires: libeconf-devel BuildRequires: libselinux-devel >= 1.25.2-1 BuildRequires: libsemanage-devel @@ -49,7 +56,6 @@ BuildRequires: libtool BuildRequires: libxcrypt-devel BuildRequires: libxslt BuildRequires: make -BuildRequires: pam BuildRequires: pam-devel ### Provides ### @@ -77,9 +83,10 @@ programs for managing user and group accounts. The pwconv command converts passwords to the shadow password format. The pwunconv command unconverts shadow passwords and generates a passwd file (a standard UNIX password file). The pwck command checks the integrity of password -and shadow files. The useradd, userdel, and usermod commands are used -for managing user accounts. The groupadd, groupdel, and groupmod -commands are used for managing group accounts. +and shadow files. The lastlog command prints out the last login times +for all users. The useradd, userdel, and usermod commands are used for +managing user accounts. The groupadd, groupdel, and groupmod commands +are used for managing group accounts. ### Subpackages ### @@ -98,7 +105,7 @@ Requires: shadow-utils-subid = %{epoch}:%{version}-%{release} Development files for shadow-utils-subid. %prep -%autosetup -p 1 -S git -n shadow-4.20.0 +%autosetup -p 1 -S git -n shadow-4.17.4 iconv -f ISO88591 -t utf-8 doc/HOWTO > doc/HOWTO.utf8 cp -f doc/HOWTO.utf8 doc/HOWTO @@ -106,12 +113,16 @@ cp -f doc/HOWTO.utf8 doc/HOWTO cp -a %{SOURCE4} %{SOURCE5} . cp -a %{SOURCE6} man/login.defs.d/HOME_MODE.xml +# Force regeneration of getdate.c +rm lib/getdate.c + %build autoreconf %configure \ - --disable-account-tools-setuid \ - --enable-logind=no \ - --enable-man \ + --disable-account-tools-setuid \ + --enable-lastlog \ + --enable-logind=no \ + --enable-man \ --enable-shadowgrp \ --enable-shared \ --with-audit \ @@ -123,21 +134,16 @@ autoreconf --with-yescrypt \ --without-libbsd \ --without-libcrack \ - --without-nscd \ + --without-nscd \ --without-sssd %make_build -%check -make check - %install %make_install gnulocaledir=$RPM_BUILD_ROOT%{_datadir}/locale MKINSTALLDIRS=`pwd`/mkinstalldirs install -d -m 755 $RPM_BUILD_ROOT%{_sysconfdir}/default install -p -c -m 0644 %{SOURCE3} $RPM_BUILD_ROOT%{_sysconfdir}/login.defs install -p -c -m 0600 %{SOURCE2} $RPM_BUILD_ROOT%{_sysconfdir}/default/useradd install -d -m 755 $RPM_BUILD_ROOT%{_pam_confdir} -install -m 644 %{SOURCE7} $RPM_BUILD_ROOT%{_pam_confdir}/chpasswd -install -m 644 %{SOURCE7} $RPM_BUILD_ROOT%{_pam_confdir}/newusers install -m 644 %{SOURCE7} $RPM_BUILD_ROOT%{_pam_confdir}/passwd @@ -155,20 +161,26 @@ mv -v $RPM_BUILD_ROOT/usr/sbin/* $RPM_BUILD_ROOT%{_bindir}/ # Remove binaries we don't use. rm $RPM_BUILD_ROOT%{_bindir}/chfn rm $RPM_BUILD_ROOT%{_bindir}/chsh +rm $RPM_BUILD_ROOT%{_bindir}/expiry rm $RPM_BUILD_ROOT%{_bindir}/login rm $RPM_BUILD_ROOT%{_bindir}/su rm $RPM_BUILD_ROOT%{_bindir}/faillog +rm $RPM_BUILD_ROOT%{_sbindir}/logoutd rm $RPM_BUILD_ROOT%{_sbindir}/nologin rm $RPM_BUILD_ROOT%{_mandir}/man1/chfn.* rm $RPM_BUILD_ROOT%{_mandir}/*/man1/chfn.* rm $RPM_BUILD_ROOT%{_mandir}/man1/chsh.* rm $RPM_BUILD_ROOT%{_mandir}/*/man1/chsh.* +rm $RPM_BUILD_ROOT%{_mandir}/man1/expiry.* +rm $RPM_BUILD_ROOT%{_mandir}/*/man1/expiry.* rm $RPM_BUILD_ROOT%{_mandir}/man1/login.* rm $RPM_BUILD_ROOT%{_mandir}/*/man1/login.* rm $RPM_BUILD_ROOT%{_mandir}/man1/su.* rm $RPM_BUILD_ROOT%{_mandir}/*/man1/su.* rm $RPM_BUILD_ROOT%{_mandir}/man5/passwd.* rm $RPM_BUILD_ROOT%{_mandir}/*/man5/passwd.* +rm $RPM_BUILD_ROOT%{_mandir}/man8/logoutd.* +rm $RPM_BUILD_ROOT%{_mandir}/*/man8/logoutd.* rm $RPM_BUILD_ROOT%{_mandir}/man8/nologin.* rm $RPM_BUILD_ROOT%{_mandir}/*/man8/nologin.* rm $RPM_BUILD_ROOT%{_mandir}/man3/getspnam.* @@ -180,8 +192,11 @@ rm $RPM_BUILD_ROOT%{_mandir}/*/man8/faillog.* # Remove PAM service files we don't use. rm $RPM_BUILD_ROOT%{_pam_confdir}/chfn +rm $RPM_BUILD_ROOT%{_pam_confdir}/chpasswd rm $RPM_BUILD_ROOT%{_pam_confdir}/chsh +rm $RPM_BUILD_ROOT%{_pam_confdir}/groupmems rm $RPM_BUILD_ROOT%{_pam_confdir}/login +rm $RPM_BUILD_ROOT%{_pam_confdir}/newusers rm $RPM_BUILD_ROOT%{_pam_confdir}/su find $RPM_BUILD_ROOT%{_mandir} -depth -type d -empty -delete @@ -208,12 +223,11 @@ rm -f $RPM_BUILD_ROOT/%{_libdir}/libsubid.a %license gpl-2.0.txt shadow-bsd.txt %attr(0644,root,root) %config(noreplace) %{_sysconfdir}/login.defs %attr(0644,root,root) %config(noreplace) %{_sysconfdir}/default/useradd -%config(noreplace) %{_pam_confdir}/chpasswd -%config(noreplace) %{_pam_confdir}/newusers %config(noreplace) %{_pam_confdir}/passwd %{_bindir}/sg %attr(4755,root,root) %{_bindir}/chage %attr(4755,root,root) %{_bindir}/gpasswd +%{_bindir}/lastlog %attr(4755,root,root) %{_bindir}/newgrp %attr(0755,root,root) %caps(cap_setgid=ep) %{_bindir}/newgidmap %attr(0755,root,root) %caps(cap_setuid=ep) %{_bindir}/newuidmap @@ -251,6 +265,7 @@ rm -f $RPM_BUILD_ROOT/%{_libdir}/libsubid.a %{_mandir}/man8/chgpasswd.8* %{_mandir}/man8/newusers.8* %{_mandir}/man8/*conv.8* +%{_mandir}/man8/lastlog.8* %{_mandir}/man8/vipw.8* %{_mandir}/man8/vigr.8* @@ -264,80 +279,9 @@ rm -f $RPM_BUILD_ROOT/%{_libdir}/libsubid.a %{_libdir}/libsubid.so %changelog -* Wed Jul 29 2026 Iker Pedrosa - 2:4.20.0-1 -- Rebase to version 4.20.0 - -* Wed Jul 22 2026 Iker Pedrosa - 2:4.20.0-rc3-1 -- Rebase to version 4.20.0-rc3 - -* Fri Jul 17 2026 Fedora Release Engineering - 2:4.19.3-4 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_45_Mass_Rebuild - -* Wed May 27 2026 Iker Pedrosa - 2:4.19.3-3 -- Enable use of PAM for chpasswd and newusers - Resolves: #2461179 and #2283963 - -* Thu Apr 23 2026 Iker Pedrosa - 2:4.19.3-2 -- btrfs: simplify checks improve useradd behavior for non-btrfs - -* Wed Feb 11 2026 Debarshi Ray - 2:4.19.3-1 -- Rebase to version 4.19.3 - Resolves: #2426288 - -* Tue Jan 27 2026 Adam Williamson - 2:4.19.0-6 -- chkhash.c: fix escaping in SHA-256 / SHA-512 / MD5 regexes - -* Mon Jan 26 2026 Iker Pedrosa - 2:4.19.0-5 -- chkhash.c: fix support for ! and * in hashes -- usermod.c: add back optimizations - -* Sat Jan 17 2026 Fedora Release Engineering - 2:4.19.0-4 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_44_Mass_Rebuild - -* Mon Jan 12 2026 Iker Pedrosa - 2:4.19.0-3 -- useradd: Support config for creating home dirs as Btrfs subvolumes - -* Fri Jan 9 2026 Iker Pedrosa - 2:4.19.0-2 -- Enable unit-tests - -* Fri Jan 9 2026 Iker Pedrosa - 2:4.19.0-1 -- Rebase to version 4.19.0 - Resolves: #2426288 and #2249524 - -* Tue Nov 25 2025 Adam Williamson - 2:4.18.0-7 -- Also revert changes from -4 (last known good was -3) - -* Tue Nov 25 2025 Adam Williamson - 2:4.18.0-6 -- Revert changes from -5 (they were only meant for testing) - -* Tue Nov 25 2025 Iker Pedrosa - 2:4.18.0-5 -- Test CI - -* Fri Oct 31 2025 Iker Pedrosa - 2:4.18.0-4 -- Stop setting SELinux labels in chroot and prefix environments - Resolves: #2249524 - -* Tue Jul 29 2025 Alexey Tikhonov - 2:4.18.0-3 -- Revert "Stop assigning subids by default" - Resolves: #2382662 - -* Fri Jul 25 2025 Fedora Release Engineering - 2:4.18.0-2 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_43_Mass_Rebuild - -* Tue Jul 22 2025 Iker Pedrosa - 2:4.18.0-1 -- Rebase to version 4.18.0. Resolves: #2374710 - -* Fri Jul 18 2025 Iker Pedrosa - 2:4.17.4-5 +* Tue Jul 22 2025 Iker Pedrosa - 2:4.17.4-2 - Stop assigning subids by default - Resolves: CVE-2024-56433 and #2334168 - -* Tue Jul 15 2025 Iker Pedrosa - 2:4.17.4-4 -- FSWC: Migrate to lastlog2 - Link: - Resolves: #2361588 - -* Tue Mar 25 2025 Iker Pedrosa - 2:4.17.4-2 -- Add pam dependency for _pam_confdir missing macro. Resolves: #2354806 + Resolves: CVE-2024-56433 and #2334169 * Thu Mar 20 2025 Iker Pedrosa - 2:4.17.4-1 - Rebase to version 4.17.4. Resolves: #2353491 diff --git a/sources b/sources index 7669083..6e720c5 100644 --- a/sources +++ b/sources @@ -1,2 +1,2 @@ -SHA512 (shadow-4.20.0.tar.xz) = 0b8afded372e4d37a78f38cb972c0ab877870ef6356cdd1c45be3c708af3d1496c6f87de6bf6a5b1d217d4d52d7e2d15c28b26530949cc6de0231a4028930406 -SHA512 (shadow-4.20.0.tar.xz.asc) = e4b134543768f323df30a1e450c22086d013020928d62bd3207db7f633beea3587b67802da5b575d5a794e8c2bebd7a0abeba6ed69d44bf08adc37df3bf352ac +SHA512 (shadow-4.17.4.tar.xz) = 06830f654650312a79ccd6d729a51808b324d594abf1c05d56a2d0880936df292ec5c9fd6c7f4ad59a6d0f2bf5be0af42afe6386c24c2c087fd64fff301bade3 +SHA512 (shadow-4.17.4.tar.xz.asc) = 24f14397a975e4b09be087705a96544ff8ad76e0aa8c708ed4a53db3a295ad0a33fd0797fc570dcbb2446d4e103a3e43922a93168f65012eba5d3fe31549ebdd diff --git a/tests/mhc-fedora-ci.yaml b/tests/mhc-fedora-ci.yaml deleted file mode 100644 index 8747e67..0000000 --- a/tests/mhc-fedora-ci.yaml +++ /dev/null @@ -1,13 +0,0 @@ -provisioned_topologies: -- shadow -domains: -- id: shadow - hosts: - - hostname: localhost - role: shadow - conn: - type: ssh - host: localhost - user: root - artifacts: - - /var/log/* diff --git a/tests/sanity/Makefile b/tests/sanity/Makefile new file mode 100644 index 0000000..386221b --- /dev/null +++ b/tests/sanity/Makefile @@ -0,0 +1,77 @@ +# Copyright (c) 2006 Red Hat, Inc. All rights reserved. This copyrighted material +# is made available to anyone wishing to use, modify, copy, or +# redistribute it subject to the terms and conditions of the GNU General +# Public License v.2. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# Author: Jakub Hrozek + +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# +# Example Makefile for RHTS # +# This example is geared towards a test for a specific package # +# It does most of the work for you, but may require further coding # +#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# + +# The toplevel namespace within which the test lives. +TOPLEVEL_NAMESPACE=CoreOS + +# The name of the package under test: +PACKAGE_NAME=shadow-utils + +# The path of the test below the package: +RELATIVE_PATH=sanity + +# Version of the Test. Used with make tag. +export TESTVERSION=1.1 + +# The combined namespace of the test. +export TEST=/$(TOPLEVEL_NAMESPACE)/$(PACKAGE_NAME)/$(RELATIVE_PATH) + +# A phony target is one that is not really the name of a file. +# It is just a name for some commands to be executed when you +# make an explicit request. There are two reasons to use a +# phony target: to avoid a conflict with a file of the same +# name, and to improve performance. +.PHONY: all install download clean + +# Executables to be built should be added here, they will be generated on the system under test. +BUILT_FILES= + +# Data files, .c files, scripts anything needed to either compile the test and/or run it. +FILES=$(METADATA) Makefile PURPOSE sanity_test.py runtest.sh + +run: $(FILES) build + ./runtest.sh + +build: $(BUILT_FILES) + chmod a+x ./sanity_test.py + chmod a+x ./runtest.sh + +clean: + rm -f *~ *.rpm $(BUILT_FILES) + +# Include Common Makefile +include /usr/share/rhts/lib/rhts-make.include + +# Generate the testinfo.desc here: +$(METADATA): Makefile + @touch $(METADATA) + @echo "Owner: Jakub Hrozek " > $(METADATA) + @echo "Name: $(TEST)" >> $(METADATA) + @echo "Path: $(TEST_DIR)" >> $(METADATA) + @echo "TestVersion: $(TESTVERSION)" >> $(METADATA) + @echo "License: GNU GPL" >> $(METADATA) + @echo "Description: Basic sanity test for shadow-utils" >> $(METADATA) + @echo "TestTime: 5m" >> $(METADATA) + @echo "RunFor: $(PACKAGE_NAME)" >> $(METADATA) + @echo "Requires: $(PACKAGE_NAME)" >> $(METADATA) + @echo "Requires: python" >> $(METADATA) + rhts-lint $(METADATA) + diff --git a/tests/sanity/PURPOSE b/tests/sanity/PURPOSE new file mode 100644 index 0000000..27062e1 --- /dev/null +++ b/tests/sanity/PURPOSE @@ -0,0 +1,10 @@ +This is a basic sanity test for the shadow-utils package. It is implemented +in python on top of the unittesting.py module. + +Its purpose is to ensure that the binaries in the shadow-utils package behave +as expected and its switches/options work correctly. + +For the most part, every binary in the shadow-utils package is represented by +a single class named Test, i.e. TestUsermod etc. There are some +exceptions, like TestUseraddWeirdNameTest though. + diff --git a/tests/sanity/runtest.sh b/tests/sanity/runtest.sh new file mode 100755 index 0000000..cb2a2b5 --- /dev/null +++ b/tests/sanity/runtest.sh @@ -0,0 +1,24 @@ +#!/bin/bash +. /usr/bin/rhts-environment.sh +. /usr/share/beakerlib/beakerlib.sh || exit 1 + +rlJournalStart +rlFileBackup --clean /etc/default/useradd- /etc/default/useradd +setenforce 0 +python sanity_test.py -v +setenforce 1 +rlFileRestore + +EXIT=$? +if [[ $EXIT -eq 0 ]]; then + RESULT="PASS" +else + RESULT="FAIL" +fi + + +rlJournalEnd + +echo "Result: $RESULT" +echo "Exit: $EXIT" +report_result $TEST $RESULT $EXIT diff --git a/tests/sanity/sanity_test.py b/tests/sanity/sanity_test.py new file mode 100755 index 0000000..e9c45c2 --- /dev/null +++ b/tests/sanity/sanity_test.py @@ -0,0 +1,1013 @@ +#!/usr/bin/env python +""" +A script that tests functionality of the shadow-utils package. + +Author: Jakub Hrozek, +License: GNU GPL v2 +Date: 2007 + +TODO: + * tests for password aging + * if something fails, print out the command issued for easier debugging + * test long options variants along with the short ones +""" + +import unittest +import pwd +import grp +import commands +import os +import os.path +import sys +import copy +import tempfile +import rpm +import shutil + +from UserDict import UserDict + +class RedHatVersion(object): + def __init__(self, type=None, version=None, release=None): + self.type = type + self.version = version + self.release = release + self.rhel = False + + def __eq__( self, other): + """ + Don't compare if either of the values is None + so we can do comparisons like 'is it fedora?' or 'is it rhel4?' + """ + ok = (self.type == other.type) + if ok == False: return False + + if self.version and other.version: + ok = (self.version == other.version) + if ok == False: return False + + if (self.release == other.release): + ok = (self.release == other.release) + + return ok + + def __ne__( self, other): + return not self.__eq__(other) + + def __get_fedora_info(self, mi): + return [ (h['version'],h['release']) for h in mi ][0] + + def __get_rhel_info(self, mi): + # The rules for RHEL versions are braindead..releases even more + ver_rpm, rel_rpm = [ (h['version'],h['release']) for h in mi ][0] + rhel_versions = { '3AS' : 3, '4AS' : 4, '5Server' : 5, '5Client' : 5, '6' : 6 } + if ver_rpm[:3] == '5.9' or ver_rpm[:1] == '6': # rhel6 prerelease and release hack + rhel_versions[ver_rpm] = 6 + if ver_rpm in rhel_versions.keys(): + return (rhel_versions[ver_rpm], rel_rpm) + + def is_rhel(self): + return self.rhel + + def get_info(self): + """ + Returns a tuple containing (type, version, release) of RHEL or Fedora. + Type is either RHEL or Fedora. + Returns None if it cannot parse the info + """ + + ts = rpm.TransactionSet() + mi = ts.dbMatch() + mi.pattern('name', rpm.RPMMIRE_GLOB, 'redhat-release*') + + if mi: + self.rhel = True + return ('RHEL',) + self.__get_rhel_info(mi) + else: + mi = ts.dbMatch('name','fedora-release') + self.rhel = False + if mi.count() != 0: + return ('Fedora',) + self.__get_fedora_info(mi) + + return None + + +class UserInfo(UserDict): + fields = { "pw_name" : 0, "pw_passwd" : 1, "pw_uid" : 2, "pw_gid" : 3, + "pw_gecos" : 4, "pw_dir" : 5, "pw_shell" : 6 } + + def __init__(self): + UserDict.__init__(self) + for f in UserInfo.fields: self[f] = None + + def __getitem__(self, key): + return UserDict.__getitem__(self, key) + + def __setitem__(self, key, value): + UserDict.__setitem__(self, key, value) + + def __cmp__(self, other): + return UserDict.__cmp__(self, other) + + def __repr__(self): + return " ; ".join( [ "%s => %s" % (k, v) for k, v in self.data.items() ] ) + + def __parse_info(self, struct): + for f in UserInfo.fields: + self[f] = struct[UserInfo.fields[f]] + + def get_info_uid(self, uid): + self.__parse_info(pwd.getpwuid(uid)) + + def get_info_name(self, name): + try: + self.__parse_info(pwd.getpwnam(name)) + except KeyError: + return None + + def lazy_compare(self, pattern): + """ Compare pattern against self. If any field in pattern is set + to None, it is automatically considered equal with the corresponding + field in self. """ + for field in UserInfo.fields: + if pattern[field] and pattern[field] != self[field]: + return False + + return True + +class GroupInfo(UserDict): + fields = { "gr_name" : 0, "gr_passwd" : 1, + "gr_gid" : 2, "gr_mem" : 3} + + def __init__(self): + UserDict.__init__(self) + for f in GroupInfo.fields: self[f] = None + + def __getitem__(self, key): + return UserDict.__getitem__(self, key) + + def __setitem__(self, key, value): + UserDict.__setitem__(self, key, value) + + def __cmp__(self, other): + return UserDict.__cmp__(self, other) + + def __repr__(self): + return " ; ".join( [ "%s => %s" % (k, v) for k, v in self.data.items() ] ) + + def __parse_info(self, struct): + for f in GroupInfo.fields: + self[f] = struct[GroupInfo.fields[f]] + + def get_info_gid(self, gid): + self.__parse_info(grp.getgrgid(gid)) + + def get_info_name(self, name): + self.__parse_info(grp.getgrnam(name)) + + def lazy_compare(self, pattern): + """ Compare pattern against self. If any field in pattern is set + to None, it is automatically considered equal with the corresponding + field in self. """ + for field in GroupInfo.fields: + if pattern[field] and pattern[field] != self[field]: + return False + + return True + +class LoginDefsParser(UserDict): + "A quick-n-dirty way how to fetch the defaults from /etc/login.defs into a dictionary" + + def __getitem__(self, key): + try: + return UserDict.__getitem__(self, key) + except KeyError: + # if a name-value is not defined in the config file, return defaults + if key == "CREATE_MAIL_SPOOL": + return "yes" + if key == "UMASK": + return "077" + + def __init__(self, path="/etc/login.defs",split=None): + self.path = path + UserDict.__init__(self) + try: + defs = open(path) + except IOError: + print "Could not open the config file %s" % (path) + + for line in defs: + if line.startswith('#'): continue + fields = line.split(split) + if len(fields) != 2: continue # yeah, we're dirty + self.data[fields[0]] = fields[1] + + def serialize(self): + output = open(self.path, "w+") + for k,v in self.data.items(): + output.write("%s=%s" % (k, v)) + + output.write("\n") + output.close() + +class TestUserInfo(unittest.TestCase): + def testLazyCompare(self): + """ (test sanity): Test comparing two UserInfo records """ + a = UserInfo() + a["pw_name"] = "foo" + a["pw_uid"] = 555 + b = copy.deepcopy(a) + c = UserInfo() + + self.assertEqual(a.lazy_compare(b), True) + self.assertEqual(a.lazy_compare(c), True) + + c["pw_name"] = "foo" + c["pw_uid"] = None + self.assertEqual(a.lazy_compare(c), True) + self.assertEqual(c.lazy_compare(a), False) + + c["pw_name"] = "bar" + self.assertNotEqual(a.lazy_compare(c), True) + + def testGetInfoUid(self): + """ (test sanity): Test getting user info based on his UID """ + a = UserInfo() + a.get_info_uid(0) + self.assertEqual(a["pw_name"], "root") + + def testGetInfoName(self): + """ (test sanity): Test getting user info based on his name """ + a = UserInfo() + a.get_info_name("root") + self.assertEqual(a["pw_uid"], 0) + +class ShadowUtilsTestBase: + """ Handy routines """ + def getDefaults(self): + # get the default values for so we can compare against that + (status, defaults_str) = commands.getstatusoutput('useradd -D') + if status != 0: + raise RuntimeError("Could not get the default values for useradd") + return dict([ rec.split("=") for rec in defaults_str.split("\n") ]) + + def getDefaultUserInfo(self, username): + expected = UserInfo() + defaults = self.getDefaults() + + expected["pw_name"] = username + expected["pw_dir"] = defaults["HOME"] + "/" + username + expected["pw_shell"] = defaults["SHELL"] + + return expected + +class TestUseradd(ShadowUtilsTestBase, unittest.TestCase): + def setUp(self): + self.username = "test-shadow-utils-useradd" + + def tearDown(self): + commands.getstatusoutput("userdel -r %s" % (self.username)) + + def testBasicAdd(self): + """ useradd: Tests basic adding of a user """ + expected = self.getDefaultUserInfo(self.username) + + runme = "useradd %s" % (self.username) + (status, output) = commands.getstatusoutput(runme) + self.failUnlessEqual(status, 0, output) + + created = UserInfo() + created.get_info_name(self.username) + self.assertEqual(created.lazy_compare(expected), True, "FAIL: Could not add a user\nIssued command: %s" % (runme)) + + def testExistingUser(self): + """ useradd: Test that user with an existing name cannot be added """ + (status, output) = commands.getstatusoutput("useradd %s" % (self.username)) + self.failUnlessEqual(status, 0, output) + self.assertNotEqual(commands.getstatusoutput("useradd %s" % (self.username))[0], 0, "FAIL: User that already exists added") + + def testCustomUID(self): + """ useradd: Adding an user with a specific UID """ + UID = 23456 # FIXME - test for a free UID slot first + + expected = self.getDefaultUserInfo(self.username) + expected["pw_uid"] = UID + + runme = "useradd %s -u %d" % (self.username, UID) + (status, output) = commands.getstatusoutput(runme) + self.failUnlessEqual(status, 0, "Issued command: %s\n" % (runme) + "Got from useradd: %s\n" % (output)) + + created = UserInfo() + created.get_info_name(self.username) + self.assertEqual(created.lazy_compare(expected), True, "FAIL: Could not add a user with a specific UID\nIssued command: %s" % (runme)) + + def testNegativeUID(self): + """ useradd: Tests that user cannot have a negative UID assigned """ + self.assertNotEqual(commands.getstatusoutput("useradd %s --uid -5" % (self.username))[0], 0, "FAIL: User with UID < 0 added") + + def testCustomExistingUID(self): + """ useradd: Adding a user with a specific existing UID """ + UID = 32112 + + expected = self.getDefaultUserInfo(self.username) + expected["pw_uid"] = UID + + (status_u, output_u) = commands.getstatusoutput("useradd %s -u %d" % (self.username, UID)) + + # must fail without -o flag + (status_u_no_o, output_u_no_o) = commands.getstatusoutput("useradd foo -u %d" % (UID)) + + # must pass with -o flag + (status_o, output_o) = commands.getstatusoutput("useradd foo -u %d -o" % (UID)) + + # clean up + (status, output) = commands.getstatusoutput("userdel -r foo") + + self.failUnlessEqual(status_u, 0, "FAIL: cannot add an user with a specified UID\n"+output_u) + self.assertEqual(status_o, 0, "FAIL: cannot add an user with an existing UID using the -o flag\n"+output_o) + self.failUnlessEqual(status, 0, output) + self.assertNotEqual(status_u_no_o, 0, "FAIL: user with an existing UID added\n"+output_u_no_o) + + def testCustomGID(self): + """ useradd: Adding an user with a specific GID """ + GID = 100 # users group should be everywhere - should we test before? + expected = self.getDefaultUserInfo(self.username) + expected["pw_gid"] = GID + + (status, output) = commands.getstatusoutput("useradd %s -g %d" % (self.username, GID)) + self.failUnlessEqual(status, 0, output) + + created = UserInfo() + created.get_info_name(self.username) + self.assertEqual(created.lazy_compare(expected), True, "FAIL: Could not add a user with a specific GID") + + def testCustomShell(self): + """ useradd: Adding an user with a specific login shell """ + shell = "/bin/ksh" + expected = self.getDefaultUserInfo(self.username) + expected["pw_shell"] = shell + + (status, output) = commands.getstatusoutput("useradd %s -s %s" % (self.username, shell)) + self.failUnlessEqual(status, 0, output) + + created = UserInfo() + created.get_info_name(self.username) + self.assertEqual(created.lazy_compare(expected), True, "FAIL: Could not add a user with a specific shell") + + def testCustomHome(self): + """ useradd: Adding an user with a specific home directory """ + home = "/tmp/useradd-test" + os.mkdir(home) + expected = self.getDefaultUserInfo(self.username) + expected["pw_dir"] = home + + (status, output) = commands.getstatusoutput("useradd %s -d %s" % (self.username, home)) + shutil.rmtree(home) + self.failUnlessEqual(status, 0, output) + + created = UserInfo() + created.get_info_name(self.username) + self.assertEqual(created.lazy_compare(expected), True, "FAIL: Could not add a user with a specific home") + + def testSystemAccount(self): + """ useradd: Adding a system user (UID < UID_MIN from /etc/login.defs) """ + defaults = LoginDefsParser() + + # system account with no home dir + expected = self.getDefaultUserInfo(self.username) + + (status, output) = commands.getstatusoutput("useradd -r %s" % (self.username)) + self.failUnlessEqual(status, 0, output) + + created = UserInfo() + created.get_info_name(self.username) + self.assertEqual(os.path.exists(created["pw_dir"]), False, "FAIL: System user has a home dir created") + self.assertEqual(created["pw_uid"] < defaults['UID_MIN'], True, "FAIL: System user has UID > UID_MIN") + self.assertEqual(created.lazy_compare(expected), True, "FAIL: Could not add a system user") + + def testAddToMoreGroups(self): + """ useradd: Creating an user that belongs to more than one group """ + (status, output) = commands.getstatusoutput("useradd -G bin %s" % (self.username)) + self.failUnlessEqual(status, 0, output) + + gr_bin = GroupInfo() + gr_bin.get_info_name("bin") + self.assertEqual(self.username in gr_bin["gr_mem"], True, "FAIL: User not in supplementary group after usermod -G -a") + + + def testAddWithCommonName(self): + """ useradd: Specifying a comment (user for account name) """ + comment = "zzzzzz" + (status, output) = commands.getstatusoutput("useradd -c %s %s" % (comment, self.username)) + self.failUnlessEqual(status, 0, output) + + created = UserInfo() + created.get_info_name(self.username) + self.assertEqual(created["pw_gecos"], comment, "FAIL: failed to create a user with a GECOS comment") + + def testHomePermissions(self): + """ useradd: Check if permissions on newly created home dir match the umask """ + defaults = LoginDefsParser() + + (status, output) = commands.getstatusoutput("useradd %s" % (self.username)) + self.failUnlessEqual(status, 0, output) + + created = UserInfo() + created.get_info_name(self.username) + + import stat + perm = os.stat(created["pw_dir"])[stat.ST_MODE] + mode = int(oct(perm & 0777)) + + self.assertEqual(defaults["UMASK"], "077", "FAIL: umask setting is not sane - is %s, should be 077" % (defaults["UMASK"])) + self.assertEqual(int(defaults["UMASK"]) + mode , 777, "FAIL: newly-created home dir does not match the umask") + + def testCreateMailSpool(self): + """ useradd: Check whether the mail spool gets created when told to""" + # set up creating of mail spool + defaults = LoginDefsParser("/etc/default/useradd", split="=") + + create_mail = defaults["CREATE_MAIL_SPOOL"] + defaults["CREATE_MAIL_SPOOL"] = "yes" + defaults.serialize() + + login_defs = LoginDefsParser() + + (status, output) = commands.getstatusoutput("useradd %s" % (self.username)) + self.failUnlessEqual(status, 0, output) + + # clean up + defaults["CREATE_MAIL_SPOOL"] = create_mail + defaults.serialize() + self.assertEqual(os.path.exists(login_defs["MAIL_DIR"] + "/" + self.username), True, "FAIL: useradd did not create mail spool") + + def testDefaultMailSettings(self): + """ useradd: Check whether the mail spool is on by default""" + defaults = LoginDefsParser("/etc/default/useradd", split="=") + self.assertEqual(defaults["CREATE_MAIL_SPOOL"], "yes\n") + + def testNoLastlog(self): + """ useradd: Check if the -l option prevents from being added to the lastlog """ + pass # FIXME - add some code here + + +class TestUseraddWeirdNameTest(unittest.TestCase, ShadowUtilsTestBase): + """ Tests addition/removal of usernames that have proven to be problematic in the past. + The reason to separate these from the main useradd test suite is to not run the setUp + and tearDown methods """ + + def addAndRemove(self, username, success=True): + expected = self.getDefaultUserInfo(username) + expected["pw_name"] = username + + (status, output) = commands.getstatusoutput("useradd %s" % (username)) + if success: + self.failUnlessEqual(status, 0, output) + else: + self.failIfEqual(status, 0, output) + return True + + created = UserInfo() + created.get_info_name(username) + self.assertEqual(created.lazy_compare(expected), True, "FAIL: TestUseraddWeirdName::addAndRemove - could not add a user") + + # the cleanup method won't help this time + (status, output) = commands.getstatusoutput("userdel -r %s" % (username)) + self.failUnlessEqual(status, 0, output) + + def testNumericName(self): + """ useradd: Test if an user with a purely numerical name can be added (123) """ + return self.addAndRemove("123") + + def testSambaName(self): + """ useradd: Test if an user with a name with a dollar at the end can be added (joepublic$ ) """ + return self.addAndRemove("joepublic$") + + def testDotInName(self): + """ useradd: Test if an user with a name with a dot in it can be added (joe.public ) """ + return self.addAndRemove("joe.public") + + def testAtInName(self): + """ useradd: Test if an user with an '@' in name can be added (joe@public.com) - should fail """ + return self.addAndRemove("joe@public.com", False) + + def testUppercase(self): + """ useradd: Test if an user with UPPERCASE or Uppercase name can be added """ + return self.addAndRemove("JOEPUBLIC") + return self.addAndRemove("Joepublic") + +class TestUseraddDefaultsChange(unittest.TestCase, ShadowUtilsTestBase): + def testDefaultsChange(self): + """ useradd: Test overriding default settings (shell, home dir, group) with a -D option """ + save = self.getDefaults() + + new_defs = dict() + new_defs["SHELL"] = "/bin/ksh" + new_defs["GROUP"] = "1" + new_defs["HOME"] = "/tmp" + + command = "useradd -D -s%s -g%s -b%s" % (new_defs["SHELL"], new_defs["GROUP"], new_defs["HOME"]) + (status, output) = commands.getstatusoutput(command) + self.failUnlessEqual(status, 0, output) + + overriden = self.getDefaults() + [ self.assertEqual(overriden[k], new_defs[k]) for k in new_defs.keys() ] + + command = "useradd -D -s%s -g%s -b%s" % (save["SHELL"], save["GROUP"], save["HOME"]) + (status, output) = commands.getstatusoutput(command) + self.failUnlessEqual(status, 0, output) + + +class TestUserdel(unittest.TestCase, ShadowUtilsTestBase): + def setUp(self): + self.username = "test-shadow-utils-userdel" + (status, output) = commands.getstatusoutput("useradd %s" % (self.username)) + self.failUnlessEqual(status, 0, output) + + def testRemoveUserGroup(self): + """ userdel: test if userdel removes user's group when he's deleted - regression test for #201379 """ + (status, output) = commands.getstatusoutput("userdel -r %s" % (self.username)) + self.failUnlessEqual(status, 0, output) + + # This would fail if we did not have the group removed + (status, output) = commands.getstatusoutput("useradd %s" % (self.username)) + self.failUnlessEqual(status, 0, output) + + (status, output) = commands.getstatusoutput("userdel -r %s" % (self.username)) + self.failUnlessEqual(status, 0, output) + +class TestUsermod(unittest.TestCase, ShadowUtilsTestBase): + def setUp(self): + self.username = "test-shadow-utils-usermod" + (status, output) = commands.getstatusoutput("useradd %s" % (self.username)) + self.failUnlessEqual(status, 0, output) + + def tearDown(self): + (status, output) = commands.getstatusoutput("userdel -r %s" % (self.username)) + self.failUnlessEqual(status, 0, output) + + def testAppendToSupplementaryGroup(self): + """ usermod: Test if a user can be added to a supplementary group """ + add_group = "additional_group" + (status, output) = commands.getstatusoutput("groupadd %s" % (add_group)) + self.failUnlessEqual(status, 0, output) + + (status_mod, output_mod) = commands.getstatusoutput("usermod -a -G %s %s" % (add_group, self.username)) + add_group_info = GroupInfo() + add_group_info.get_info_name(add_group) + (status, output) = commands.getstatusoutput("groupdel %s" % (add_group)) + + self.failUnlessEqual(status, 0, output) + self.failUnlessEqual(status_mod, 0, output_mod) + self.assertEqual(self.username in add_group_info["gr_mem"], True, "User not in supplementary group after usermod -G --append") + + + def testAppendToSupplementaryGroupLongOption(self): + """ usermod: Test if a user can be added to a supplementary group via --append rather that -a (regression test for 222540) """ + # this is known to not work on older RHELs - test what we are running + rhv = RedHatVersion() + runs = rhv.get_info() + if rhv.is_rhel(): + if runs[1] < 5: + print "This test makes sense for RHEL5+" + return + else: + if runs[1] < 6: + print "This test makes sense for Fedora 6+" + return + + type, release, version = RedHatVersion().get_info() + if RedHatVersion().is_rhel(): + if release < 5 or (release == 5 and version < 2): + print "This test makes sense for RHEL 5.2+" + return + + add_group = "additional_group" + (status, output) = commands.getstatusoutput("groupadd %s" % (add_group)) + self.failUnlessEqual(status, 0, output) + + (status_mod, output_mod) = commands.getstatusoutput("usermod --append -G %s %s" % (add_group, self.username)) + add_group_info = GroupInfo() + add_group_info.get_info_name(add_group) + (status, output) = commands.getstatusoutput("groupdel %s" % (add_group)) + + self.failUnlessEqual(status, 0, output) + self.failUnlessEqual(status_mod, 0, output_mod) + self.assertEqual(self.username in add_group_info["gr_mem"], True, "User not in supplementary group after usermod -G --append") + + + def testNameChange(self): + """ usermod: Test if the comment field (used as the Common Name) can be changed """ + new_comment = "zzzzzz" + + (status, output) = commands.getstatusoutput("usermod -c %s %s" % (new_comment, self.username)) + self.failUnlessEqual(status, 0, output) + + created = UserInfo() + created.get_info_name(self.username) + + self.assertEqual(created["pw_gecos"], new_comment) + + def testHomeChange(self): + """ usermod: Test if user's home directory can be changed """ + new_home = "/tmp" + created = UserInfo() + created.get_info_name(self.username) + old_home = created["pw_dir"] + + (status, output) = commands.getstatusoutput("usermod -d %s %s" % (new_home, self.username)) + self.failUnlessEqual(status, 0, output) + + created.get_info_name(self.username) + self.assertEqual(created["pw_dir"], new_home) + + # revert to old home so we can userdel -r in tearDown + (status, output) = commands.getstatusoutput("usermod -d %s %s" % (old_home, self.username)) + self.failUnlessEqual(status, 0, output) + + # FIXME - test if contents of /home directories are transferred with the -m option + # FIXME - test if new home is created if does not exist before + + def testGIDChange(self): + """ usermod: Test if user's gid can be changed. """ + new_group = "root" + # test non-existing group + (status_fail, output_fail) = commands.getstatusoutput("usermod -g no-such-group %s" % (self.username)) + (status, output) = commands.getstatusoutput("usermod -g %s %s" % (new_group, self.username)) + + created = UserInfo() + created.get_info_name(self.username) + + left = GroupInfo() + if left.get_info_name(self.username) == None: + (status_del, output_del) = commands.getstatusoutput("groupdel %s" % (self.username)) + self.failUnlessEqual(status_del, 0, output_del) + + self.failIfEqual(status_fail, 0, output_fail) + self.failUnlessEqual(status, 0, output) + self.assertEqual(created["pw_gid"], 0) #0 is root group + + def testLoginChange(self): + """ usermod: Test if user's login can be changed """ + new_login = "usermod-login-change" + user = UserInfo() + user.get_info_name(self.username) + uid = user["pw_uid"] # UID won't change even when login does + + # test changing to an existing user name + (status, output) = commands.getstatusoutput("usermod -l root %s" % (self.username)) + self.failIfEqual(status, 0, output) + + (status, output) = commands.getstatusoutput("usermod -l %s %s" % (new_login, self.username)) + self.failUnlessEqual(status, 0, output) + user.get_info_name(new_login) + self.assertEqual(user["pw_uid"], uid) + + # revert so we can userdel -r on tearDown + (status, output) = commands.getstatusoutput("usermod -l %s %s" % (self.username, new_login)) + self.failUnlessEqual(status, 0, output) + + def testShellChange(self): + """ usermod: Test if user's shell can be changed """ + new_shell = "/bin/sh" + + (status, output) = commands.getstatusoutput("usermod -s %s %s" % (new_shell, self.username)) + self.failUnlessEqual(status, 0, output) + + created = UserInfo() + created.get_info_name(self.username) + self.assertEqual(created["pw_shell"], new_shell) + +class TestGroupadd(unittest.TestCase, ShadowUtilsTestBase): + def setUp(self): + self.groupname = "test-shadow-utils-groups" + + def tearDown(self): + commands.getstatusoutput("groupdel %s" % (self.groupname)) + + def testAddGroup(self): + """ groupadd: Basic adding of a group """ + + expected = GroupInfo() + expected["gr_name"] = self.groupname + + (status, output) = commands.getstatusoutput("groupadd %s" % (self.groupname)) + self.failUnlessEqual(status, 0, output) + + created = GroupInfo() + created.get_info_name(self.groupname) + self.assertEqual(created.lazy_compare(expected), True, "FAIL: Could not add a group") + + def testAddSystemGroup(self): + """ groupadd: Adding a system group with gid < MIN_GID """ + + expected = GroupInfo() + expected["gr_name"] = self.groupname + defaults = LoginDefsParser() + + (status, output) = commands.getstatusoutput("groupadd -r %s" % (self.groupname)) + self.failUnlessEqual(status, 0, output) + + created = GroupInfo() + created.get_info_name(self.groupname) + self.assertEqual(created["gr_gid"] < defaults["GID_MIN"], True, "FAIL: System group has gid >= GID_MIN") + self.assertEqual(created.lazy_compare(expected), True, "FAIL: Could not add a system group") + + def testAddExistingGid(self): + """ groupadd: Test if we group with an existing GID can be added """ + (status, output) = commands.getstatusoutput("groupadd %s" % (self.groupname)) + self.failUnlessEqual(status, 0, output) + + gname = "%s-2" % (self.groupname) + + created = GroupInfo() + created.get_info_name(self.groupname) + + # no -o option -> this should fail + (status, output) = commands.getstatusoutput("groupadd -g%s %s" % (created["gr_gid"], gname)) + self.failIfEqual(status, 0, output) + + # override with -o option, should pass now + (status, output) = commands.getstatusoutput("groupadd -g%s -o %s" % (created["gr_gid"], gname)) + self.failUnlessEqual(status, 0, output) + + # test if the new GID is really the same + same_gid = GroupInfo() + same_gid.get_info_name(gname) + self.assertEqual(same_gid["gr_gid"], created["gr_gid"]) + + # clean up + (status, output) = commands.getstatusoutput("groupdel %s" % (gname)) + self.failUnlessEqual(status, 0, output) + + + def testOverrideDefaults(self): + """ groupadd: Test if the defaults can be overriden with the -K option """ + # this is known to not work on older RHELs - test what we are running + rhv = RedHatVersion() + runs = rhv.get_info() + if rhv.is_rhel(): + if runs[1] < 5: + print "This test makes sense for RHEL5+" + return + else: + if runs[1] < 6: + print "This test makes sense for Fedora 6+" + return + + + GID_MIN = 600 + GID_MAX = 625 + + (status, output) = commands.getstatusoutput("groupadd -K GID_MIN=%d -K GID_MAX=%d %s" % + (GID_MIN, GID_MAX, self.groupname)) + self.failUnlessEqual(status, 0, output) + + created = GroupInfo() + created.get_info_name(self.groupname) + self.assertEqual(GID_MIN <= created["gr_gid"] <= GID_MAX, True, "FAIL: created an user with UID of %d" % (created["gr_gid"])) + + + def testFOption(self): + """ groupadd: Tests the -f option of groupadd """ + (status, output) = commands.getstatusoutput("groupadd %s" % (self.groupname)) + self.failUnlessEqual(status, 0, output) + + (status, output) = commands.getstatusoutput("groupadd -f %s" % (self.groupname)) + self.assertEqual(status, 0, output) + +class TestGroupaddInvalidName(unittest.TestCase, ShadowUtilsTestBase): + def testGroupaddInvalidName(self): + """ groupadd: Test adding of a group with an invalid name """ + (status, output) = commands.getstatusoutput("groupadd foo?") + self.assertNotEqual(status, 0, output) + (status, output) = commands.getstatusoutput("groupadd aaaaabbbbbcccccdddddeeeeefffffggg") #33 chars + self.assertNotEqual(status, 0, output) + +class TestGroupaddValidName(unittest.TestCase, ShadowUtilsTestBase): + def testGroupaddValidName(self): + """ groupadd: Test adding and removing of groups with maximal valid name and name ending with $ """ + (status, output) = commands.getstatusoutput("groupadd aaaaabbbbbcccccdddddeeeeefffffgg") #32 chars + self.assertEqual(status, 0, output) + (status, output) = commands.getstatusoutput("groupadd aaaaabbbbbcccccdddddeeeeefffffg\$") #32 chars + self.assertEqual(status, 0, output) + (status, output) = commands.getstatusoutput("groupdel aaaaabbbbbcccccdddddeeeeefffffgg") #32 chars + self.assertEqual(status, 0, output) + (status, output) = commands.getstatusoutput("groupdel aaaaabbbbbcccccdddddeeeeefffffg\$") #32 chars + self.assertEqual(status, 0, output) + + +class TestGroupmod(unittest.TestCase, ShadowUtilsTestBase): + def setUp(self): + self.groupname = "test-shadow-utils-groups" + (status, output) = commands.getstatusoutput("groupadd %s" % (self.groupname)) + self.failUnlessEqual(status, 0, output) + + def tearDown(self): + commands.getstatusoutput("groupdel %s" % (self.groupname)) + + def testChangeGID(self): + """ groupmod: Test changing a gid of a group """ + expected = GroupInfo() + expected["gr_name"] = self.groupname + expected["gr_gid"] = 54321 + + (status, output) = commands.getstatusoutput("groupmod -g%d %s" % (expected["gr_gid"], self.groupname)) + self.failUnlessEqual(status, 0, output) + + created = GroupInfo() + created.get_info_name(self.groupname) + self.assertEqual(created.lazy_compare(expected), True, "FAIL: Could not change GID of an existing group") + + def testChangeGIDToExistingValue(self): + """ groupmod: Test changing GID to an existing value """ + second_name = "%s-2" % (self.groupname) + + created = GroupInfo() + created.get_info_name(self.groupname) + + expected = GroupInfo() + expected["gr_name"] = self.groupname + expected["gr_gid"] = created["gr_gid"] + + (status, output) = commands.getstatusoutput("groupadd %s" % (second_name)) + self.failUnlessEqual(status, 0, output) + + # try to assingn GID of the first group to the second - this should fail without the -o option + (status, output) = commands.getstatusoutput("groupmod -g%d %s" % (created["gr_gid"], second_name)) + self.failIfEqual(status, 0, output) + + # should pass with the -o option + (status, output) = commands.getstatusoutput("groupmod -g%d -o %s" % (created["gr_gid"], second_name)) + self.failUnlessEqual(status, 0, output) + + self.assertEqual(created.lazy_compare(expected), True, "FAIL: Could not change GID of an existing group to an existing one") + + # clean up + commands.getstatusoutput("groupdel %s" % (second_name)) + self.failUnlessEqual(status, 0, output) + + def testChangeGroupName(self): + """ groupmod: Test changing a group's name """ + second_name = "%s-2" % (self.groupname) + + created = GroupInfo() + created.get_info_name(self.groupname) + + (status, output) = commands.getstatusoutput("groupmod -n%s %s" % (second_name, self.groupname)) + self.failUnlessEqual(status, 0, output) + + changed = GroupInfo() + changed.get_info_gid(created["gr_gid"]) + self.assertEqual(changed["gr_name"], second_name) + self.assertEqual(changed["gr_gid"], created["gr_gid"]) + + # change back, so the group could be deleted by tearDown + (status, output) = commands.getstatusoutput("groupmod -n%s %s" % (self.groupname, second_name)) + self.failUnlessEqual(status, 0, output) + + def testChangeGroupNameExisting(self): + """ groupmod: Test changing a group's name to an existing one """ + existing = "bin" + (status, output) = commands.getstatusoutput("groupmod -n%s %s" % (existing, self.groupname)) + self.assertNotEqual(status, 0, output) # man groupmod -> 9: group name already in use + + def testChangeNonExistingGroup(self): + """ groupmod: Test properties of a non-existing group """ + nonexistent = "foobar" + (status, output) = commands.getstatusoutput("groupmod -nspameggs %s" % (nonexistent)) + self.assertNotEqual(status, 0, status) # man groupmod -> 6: specified group doesn't exist + +class TestGroupdel(unittest.TestCase, ShadowUtilsTestBase): + def testCorrectGroupdel(self): + """ groupdel: Basic usage of groupdel """ + self.groupname = "test-shadow-utils-groups" + (status, output) = commands.getstatusoutput("groupadd %s" % (self.groupname)) + self.failUnlessEqual(status, 0, output) + (status, output) = commands.getstatusoutput("groupdel %s" % (self.groupname)) + self.assertEqual(status, 0, output) + + def testGroupdelNoSuchGroup(self): + """ groupdel: Remove non-existing group """ + (status, output) = commands.getstatusoutput("groupdel foobar") + self.assertNotEqual(status, 0, output) + + def testRemovePrimaryGroup(self): + """ groupdel: Remove a primary group of an user """ + username = "test-groupdel-primary" + (status, output) = commands.getstatusoutput("useradd %s" % (username)) + self.failUnlessEqual(status, 0, output) + + (status, output) = commands.getstatusoutput("groupdel %s" % (username)) + self.assertNotEqual(status, 0, output) + + # clean up + (status, output) = commands.getstatusoutput("userdel -r %s" % (username)) + self.failUnlessEqual(status, 0, output) + +class TestPwckGrpck(unittest.TestCase): + def setUp(self): + self.passwd_path = tempfile.mktemp(suffix="test-pwck-passwd") + self.passwd_file = open(self.passwd_path, "w") + self.group_path = tempfile.mktemp(suffix="test-pwck-grp") + self.group_file = open(self.group_path, "w") + self.gshadow_path = tempfile.mktemp(suffix="test-pwck-gshadow") + self.gshadow_file = open(self.gshadow_path, "w") + + def tearDown(self): + self.passwd_file.close() + self.group_file.close() + self.gshadow_file.close() + + os.remove(self.passwd_path) + os.remove(self.group_path) + os.remove(self.gshadow_path) + + def runPwckCheck(self, passwd, group): + self.passwd_file.truncate() + self.group_file.truncate() + + self.passwd_file.write(passwd) + self.passwd_file.flush() + self.group_file.write(group) + self.group_file.flush() + + command = "pwck -r %s %s" % (self.passwd_path, self.group_path) + return commands.getstatusoutput(command) + + def runGrpCheck(self, group, gshadow): + self.group_file.truncate() + self.gshadow_file.truncate() + + self.gshadow_file.write(gshadow) + self.gshadow_file.flush() + + self.group_file.write(group) + self.group_file.flush() + + command = "grpck -r %s %s" % (self.group_path, self.gshadow_path) + return commands.getstatusoutput(command) + + + def testValidEntries(self): + """ pwck: a valid entry """ + status, output = self.runPwckCheck("foo:x:685:0::/dev/null:/bin/bash", "") + rhv = RedHatVersion() + runs = rhv.get_info() + if rhv.is_rhel(): + if runs[1] < 6: + self.assertEqual(status, 0, output) + else: + self.assertNotEqual(status, 0, output) + + def testNumberOfFields(self): + """ pwck: invalid number of fields in the record """ + not_enough = "foo:x:685:685::/dev/null" + too_many = "foo:x:685:685::/dev/null:/bin/bash:comment" + status, output = self.runPwckCheck(not_enough, "") + self.assertNotEqual(status, 0, output) + + status, output = self.runPwckCheck(too_many, "") + self.assertNotEqual(status, 0, output) + + def testUniqueUserName(self): + """ pwck: unique user name in the record """ + duplicate_username = "foo:x:685:685::/dev/null:/bin/bash\nfoo:x:686:686::/dev/null:/bin/bash" + status, output = self.runPwckCheck(duplicate_username, "") + self.assertNotEqual(status, 0, output) + + def testValidID(self): + """ pwck: invalid UID in the records """ + invalid_ids = [ "foo:x:-1:685::/dev/null:/bin/bash", "foo:x:blah:685::/dev/null:/bin/bash", "foo:x:1234567890:685::/dev/null:/bin/bash" ] + for record in invalid_ids: + status, output = self.runPwckCheck(record, "") + self.assertNotEqual(status, 0, record) + + + def testValidPrimaryGroup(self): + """ pwck: invalid primary group """ + invalid_groups = [ "foo:x:685:-1::/dev/null:/bin/bash", "foo:x:685:blah::/dev/null:/bin/bash", "foo:x:685:1234567890::/dev/null:/bin/bash" ] + for record in invalid_groups: + status, output = self.runPwckCheck("", record) + self.assertNotEqual(status, 0, output) + + def testValidHomeDir(self): + """ pwck: invalid home dir """ + for record in [ "foo:x:685:685::123:/bin/bash", "foo:x:685:685::/path/to/nowhere:/bin/bash", "foo:x:685:1234567890::!:/bin/bash" ]: + status, output = self.runPwckCheck(record, "") + self.assertNotEqual(status, 0, output) + + def testBZ164954(self): + """ grpck: regression test for BZ164954 """ + record = "root:x:0:root\nbin:x:1:root,bin,daemon\ndaemon:x:2:root,bin,daemon\nsys:x:3:root,bin,adm\nadm:x:4:root,adm,daemon" + status, output = self.runGrpCheck("", record) + self.assertNotEqual(status, 0, output) + +if __name__ == "__main__": + broken_on_rhel4 = { "TestUseradd" : [ "testCustomUID", "testCustomGID" ] } + + if os.getuid() != 0: + print "This test must be run as root" + sys.exit(1) + + unittest.main() + diff --git a/tests/tests.yml b/tests/tests.yml new file mode 100644 index 0000000..09f4769 --- /dev/null +++ b/tests/tests.yml @@ -0,0 +1,13 @@ +--- +# This first play always runs on the local staging system +- hosts: localhost + roles: + - role: standard-test-beakerlib + tags: + - classic + - atomic + tests: + - sanity + required_packages: + - shadow-utils # sanity test needs shadow-utils + - python # sanity test needs python