From 6d97e51b7c90cd43d4ee7cb5ced48b0f54da9f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Santos=20Reis?= Date: Wed, 6 May 2026 10:09:37 +0100 Subject: [PATCH] shadow: enable running upstream system test suite --- nixos/tests/shadow/default.nix | 1 + nixos/tests/shadow/system.nix | 147 +++++++++++++++++++++++++++++ pkgs/by-name/sh/shadow/package.nix | 54 ++++++++++- 3 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 nixos/tests/shadow/system.nix diff --git a/nixos/tests/shadow/default.nix b/nixos/tests/shadow/default.nix index 703f1119d824..ba773f93f073 100644 --- a/nixos/tests/shadow/default.nix +++ b/nixos/tests/shadow/default.nix @@ -1,4 +1,5 @@ { runTest }: { login = runTest ./login.nix; + system = runTest ./system.nix; } diff --git a/nixos/tests/shadow/system.nix b/nixos/tests/shadow/system.nix new file mode 100644 index 000000000000..fbed9be8beba --- /dev/null +++ b/nixos/tests/shadow/system.nix @@ -0,0 +1,147 @@ +{ pkgs, ... }: +let + # Create a Python environment for the controller with all necessary test framework dependencies + controllerPython = pkgs.python3.withPackages (ps: [ + ps.flaky + ps.jc + ps.pytest + ps.pytest-mh + ps.pytest-ticket + ]); + + shadowHostName = "shadowhost"; +in +{ + name = "shadow-system-tests"; + + meta.maintainers = with pkgs.lib.maintainers; [ joaosreis ]; + + nodes = { + # The target host: runs sshd, has shadow and other test dependencies installed, mutable users, and some specific settings to match the expectations of the test suite + shadowhost = + { pkgs, ... }: + { + networking.hostName = shadowHostName; + services.openssh = { + enable = true; + settings = { + PermitRootLogin = "yes"; + PasswordAuthentication = false; + }; + }; + + users.mutableUsers = true; + + environment.systemPackages = with pkgs; [ + shadow + expect + ]; + + users.defaultUserShell = "/bin/sh"; + + security.loginDefs.settings = { + PASS_MAX_DAYS = 99999; + PASS_MIN_DAYS = 0; + PASS_WARN_AGE = 7; + USERGROUPS_ENAB = "yes"; + CREATE_HOME = "yes"; + UID_MIN = 1001; + GID_MIN = 1001; + }; + + security.pam.services = { + newusers.text = '' + auth required pam_permit.so + account required pam_permit.so + password required pam_permit.so + session required pam_permit.so + ''; + }; + + services.envfs.enable = true; + }; + + # The controller: runs pytest-mh against the shadow host + controller = + { pkgs, ... }: + { + environment.systemPackages = [ + controllerPython + pkgs.openssh + ]; + }; + + }; + + testScript = '' + import textwrap + + start_all() + + # ------------------------------------------------------------------ + # 1. Generate an SSH keypair on the controller and authorise it + # on the shadow host + # ------------------------------------------------------------------ + controller.succeed("mkdir -p /root/.ssh && chmod 700 /root/.ssh") + controller.succeed( + "ssh-keygen -t ed25519 -N \'\' -f /root/.ssh/id_ed25519 2>&1" + ) + pub_key = controller.succeed("cat /root/.ssh/id_ed25519.pub").strip() + + # Inject the generated public key into the shadow host at runtime + shadowhost.succeed("mkdir -p /root/.ssh && chmod 700 /root/.ssh") + shadowhost.succeed( + f"echo '{pub_key}' >> /root/.ssh/authorized_keys && " + "chmod 600 /root/.ssh/authorized_keys" + ) + + # ------------------------------------------------------------------ + # 2. Make sure the shadow host has a writable /etc/login.defs, + # since the test framework expects to be able to write to it. + # ------------------------------------------------------------------ + shadowhost.succeed( + "cp --remove-destination $(readlink -f /etc/login.defs) /etc/login.defs && " + "chmod 644 /etc/login.defs" + ) + + # ------------------------------------------------------------------ + # 3. Copy the upstream test suite onto the controller + # ------------------------------------------------------------------ + controller.succeed( + "cp -r ${pkgs.shadow.passthru.testFramework} /root/shadow-tests && " + "chmod -R u+w /root/shadow-tests" + ) + + # ------------------------------------------------------------------ + # 4. Write the mhc.yaml topology config + # This tells pytest-mh where the shadow host is and which role + # it plays. The hostname must match the NixOS node name. + # ------------------------------------------------------------------ + controller.succeed(textwrap.dedent(""" + cat > /root/shadow-tests/mhc.yaml << 'EOF' + domains: + - id: shadow + hosts: + - hostname: ${shadowHostName} + role: shadow + ssh: + user: root + private_key: /root/.ssh/id_ed25519 + EOF + """)) + + + # ------------------------------------------------------------------ + # 5. Run the upstream pytest-mh test suite from the controller + # ------------------------------------------------------------------ + shadowhost.wait_for_unit("sshd.service") + # gpasswd tests are disabled, since they rely on specific behavior of the gpasswd command that is not applicable to NixOS + controller.succeed( + "cd /root/shadow-tests && " + "${controllerPython}/bin/pytest " + "--mh-config=mhc.yaml " + "--deselect=tests/test_gpasswd.py " + "-v tests/" + ) + ''; +} diff --git a/pkgs/by-name/sh/shadow/package.nix b/pkgs/by-name/sh/shadow/package.nix index 4c5f050b911f..51a216b27684 100644 --- a/pkgs/by-name/sh/shadow/package.nix +++ b/pkgs/by-name/sh/shadow/package.nix @@ -20,6 +20,7 @@ withTcb ? lib.meta.availableOn stdenv.hostPlatform tcb, tcb, cmocka, + fetchpatch, }: let glibc' = @@ -139,7 +140,58 @@ stdenv.mkDerivation (finalAttrs: { passthru = { shellPath = "/bin/nologin"; - # TODO: Run system tests: https://github.com/shadow-maint/shadow/blob/master/doc/contributions/tests.md#system-tests tests = { inherit (nixosTests) shadow; }; + # Package the upstream system test framework for use in nixosTests + testFramework = stdenv.mkDerivation { + name = "shadow-test-framework"; + inherit (finalAttrs) version; + src = "${finalAttrs.src}/tests/system"; + installPhase = '' + cp -r . $out/ + ''; + dontBuild = true; + patches = [ + # tests: update useradd tests to expect ID 1001 + (fetchpatch { + name = "update-useradd-tests.patch"; + url = "https://github.com/shadow-maint/shadow/commit/59fbe8415dab17f1e702fbdee96956886c86c737.patch"; + hash = "sha256-U0+NSCd4AfTuHMddTx9+wNtpdJt9t8+D5ApW0OCNgsY="; + stripLen = 2; + }) + # tests: update usermod tests to expect ID 1001 + (fetchpatch { + name = "update-usermod-tests.patch"; + url = "https://github.com/shadow-maint/shadow/commit/91c2ad44ababca2e32cdb71152b0f7f2a7c546be.patch"; + hash = "sha256-v1EEvMfUYoE/ZnBM0k/+kUBK3W1dXr588OQzvFmnXLI="; + stripLen = 2; + }) + # tests: update groupadd tests to expect GID 1001 + (fetchpatch { + name = "update-groupadd-tests.patch"; + url = "https://github.com/shadow-maint/shadow/commit/60568eaec13d1e417f56f0e59ac9573c0e3b9a83.patch"; + hash = "sha256-tLmGeW4Y7UcZqMhW3IXgygKPhCmbZkkW5XTJ7CFz9vg="; + stripLen = 2; + }) + # tests: update newgrp tests to expect GID 1002 + (fetchpatch { + name = "update-newgrp-tests.patch"; + url = "https://github.com/shadow-maint/shadow/commit/49ff9bf33a7b6af57cb26688c3139f014302c9d9.patch"; + hash = "sha256-1760t4Ezd5Ke4PFZ70Njb+k+1kp17aT/7uqu1PswVss="; + stripLen = 2; + }) + ]; + postPatch = '' + # Replace the gshadow existence check in the test framework with a more NixOS-friendly one, since NixOS does not have /etc/gshadow as a regular file + substituteInPlace framework/hosts/shadow.py \ + --replace-fail 'getent gshadow > /dev/null 2>&1' 'test -f /etc/gshadow' + + # Remove the backup entry for gshadow, since it's not being used in the tests running on NixOS + sed -i '/{"origin": "\/etc\/gshadow", "backup": "gshadow"}/d' framework/hosts/shadow.py + + # Replace the Debian-specific check in the useradd test with a NixOS-specific one + substituteInPlace tests/test_useradd.py \ + --replace-fail 'if "Debian" in shadow.host.distro_name:' 'if "NixOS" in shadow.host.distro_name:' + ''; + }; }; })