Repository: osbuild/osbuild
Pull request #1778 (2024-05-09 17:29:41)

Add user force password reset
This PR is the new version of https://github.com/osbuild/osbuild/pull/1695. I am making a new PR because the old branch name is misleading given the discussion on the previous PR

elkoniu: Triggering workflows for this one.

elkoniu: LGTM - I retriggered failing Schutzbot job but it looks to be unrelated to PR.

Patch:
diff --git a/stages/org.osbuild.users b/stages/org.osbuild.users
index 67722a5d..08fbf287 100755
--- a/stages/org.osbuild.users
+++ b/stages/org.osbuild.users
@@ -110,6 +110,7 @@ def main(tree, options):
         shell = user_options.get("shell")
         password = user_options.get("password")
         expiredate = user_options.get("expiredate")
+        force_password_reset = user_options.get("force_password_reset", False)
 
         passwd = getpwnam(tree, name)
         if passwd is not None:
@@ -125,6 +126,9 @@ def main(tree, options):
         else:
             useradd(tree, name, uid, gid, groups, description, home, shell, password, expiredate)
 
+        if force_password_reset:
+            subprocess.run(["chroot", tree, "passwd", "--expire", name], check=True)
+
         # following maintains backwards compatibility for handling a single ssh key
         key = user_options.get("key")   # Public SSH key
         keys = user_options.get("keys", [])  # Additional public SSH keys
diff --git a/stages/org.osbuild.users.meta.json b/stages/org.osbuild.users.meta.json
index 6c8668a6..22df5dee 100644
--- a/stages/org.osbuild.users.meta.json
+++ b/stages/org.osbuild.users.meta.json
@@ -66,6 +66,10 @@
                 "expiredate": {
                   "description": "The date on which the user account will be disabled. This date is represented as a number of days since January 1st, 1970.",
                   "type": "integer"
+                },
+                "force_password_reset": {
+                  "description": "Force this user to change their password on first login",
+                  "type": "boolean"
                 }
               }
             }


Test patch:
diff --git a/stages/test/test_users.py b/stages/test/test_users.py
index c74cbe55..58b681e5 100644
--- a/stages/test/test_users.py
+++ b/stages/test/test_users.py
@@ -82,3 +82,70 @@ def test_users_mock_bin(tmp_path, stage_module, user_opts, expected_args):
         stage_module.main(tmp_path, options)
         assert len(mocked_chroot.call_args_list) == 1
         assert mocked_chroot.call_args_list[0][2:] == expected_args + ["foo"]
+
+
+# separate test right now as it results in two binaries being called
+# (adduser,passwd) which our parameter tests cannot do yet
+
+
+def test_users_with_password_reset_none(tmp_path, stage_module):
+    with mock_command("chroot", "") as mocked_chroot:
+        make_fake_tree(tmp_path, {
+            "/etc/passwd": "",
+        })
+
+        options = {
+            "users": {
+                "foo": {
+                },
+            }
+        }
+
+        stage_module.main(tmp_path, options)
+        assert len(mocked_chroot.call_args_list) == 1
+        assert mocked_chroot.call_args_list[0][1:] == ["useradd", "foo"]
+
+# separate test right now as it results in two binaries being called
+# (adduser,passwd) which our parameter tests cannot do yet
+
+
+def test_users_with_password_reset_false(tmp_path, stage_module):
+    with mock_command("chroot", "") as mocked_chroot:
+        make_fake_tree(tmp_path, {
+            "/etc/passwd": "",
+        })
+
+        options = {
+            "users": {
+                "foo": {
+                    "force_password_reset": False,
+                },
+            }
+        }
+
+        stage_module.main(tmp_path, options)
+        assert len(mocked_chroot.call_args_list) == 1
+        assert mocked_chroot.call_args_list[0][1:] == ["useradd", "foo"]
+
+# separate test right now as it results in two binaries being called
+# (adduser,passwd) which our parameter tests cannot do yet
+
+
+def test_users_with_password_reset_true(tmp_path, stage_module):
+    with mock_command("chroot", "") as mocked_chroot:
+        make_fake_tree(tmp_path, {
+            "/etc/passwd": "",
+        })
+
+        options = {
+            "users": {
+                "foo": {
+                    "force_password_reset": True,
+                },
+            }
+        }
+
+        stage_module.main(tmp_path, options)
+        assert len(mocked_chroot.call_args_list) == 2
+        assert mocked_chroot.call_args_list[0][1:] == ["useradd", "foo"]
+        assert mocked_chroot.call_args_list[1][1:] == ["passwd", "--expire", "foo"]