mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 06:45:09 +00:00
refactor(install): generate the web sudoers rules in one place (#622)
* refactor(install): generate the web sudoers rules in one place /etc/sudoers.d/ledmatrix_web was written by two copies of the same allow-list: a heredoc in first_time_install.sh Step 10 and a block of echo lines in scripts/install/configure_web_sudo.sh. They drifted before (safe_pip_install.sh was granted by one only), and a test existed just to catch that. Both now call web_sudoers_rules() from the new scripts/install/lib_sudoers.sh and keep their own validate (visudo -c), install and confirm flows. - first_time_install.sh output is byte-for-byte unchanged, so a device re-running the installer gets "already up to date". If the library is missing, Step 10 keeps the installed file and carries on, the same way it handles rules that fail visudo (an empty file would pass visudo). - configure_web_sudo.sh now writes the installer's layout: same 18 rules, different comments and order. It still leaves out reboot, poweroff and journalctl when they are missing; the library does that for both. The drift test now pins the generator's grants, checks that neither installer writes rules of its own, and runs each installer's call line to check the argument order. Tests that read the rule text now read the library. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(install): detect the web service user in one function first_time_install.sh pasted the same WEB_SERVICE_USER detection block three times (Step 3.1's fallback, the plugin-repos setup and Step 11). The copies were identical apart from comments; they now call detect_web_service_user(), whose body is that block unchanged. Behaviour is the same: the function sets the same global and always returns 0, as the inline if-chain did. Checked on Linux against all three original copies across 13 layouts (installed unit with and without User=, the repo as shipped, each grep branch, template placeholders). The comment notes that the install_web_service.sh / install_service.sh greps no longer match anything, so until Step 8 installs the unit the result is "root". That behaviour is left as it was. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,33 +62,135 @@ def test_configure_web_sudo_validates_before_installing():
|
||||
assert validate < install, "the rules must be checked before they are installed"
|
||||
|
||||
|
||||
def _render_first_time_sudoers(project_root, user):
|
||||
"""Run the installer's own sudoers heredoc with realistic values."""
|
||||
def test_a_missing_rules_library_installs_nothing():
|
||||
"""If lib_sudoers.sh is missing, nothing is generated -- and an empty file
|
||||
would pass `visudo -c` -- so that branch must set the flag the install is
|
||||
gated on."""
|
||||
body = _read(FIRST_TIME)
|
||||
start = body.index("# Create sudoers content")
|
||||
missing = body.index('if [ -f "$SUDOERS_LIB" ]; then')
|
||||
flagged = body.index("SUDOERS_VALID=0", missing)
|
||||
validate = body.index('visudo -c -f "$SUDOERS_TMP"')
|
||||
install = body.index('cp "$SUDOERS_TMP" "$SUDOERS_FILE"')
|
||||
gate = body.rindex('if [ "$SUDOERS_VALID" = "0" ]; then', 0, install)
|
||||
assert missing < flagged < validate < gate < install
|
||||
|
||||
|
||||
def _step10_generation(body):
|
||||
"""first_time_install.sh's own Step 10 code that writes $SUDOERS_TMP."""
|
||||
start = body.index("# The rules themselves live in scripts/install/lib_sudoers.sh")
|
||||
end = body.index("# Never install rules we have not parsed.")
|
||||
block = body[start:end]
|
||||
out = os.path.join(project_root, "rendered")
|
||||
return body[start:end]
|
||||
|
||||
|
||||
def _run_step10_generation(project_root, user, out):
|
||||
"""Run the installer's Step 10 generation with realistic values.
|
||||
|
||||
Returns the SUDOERS_VALID it leaves behind."""
|
||||
script = "\n".join(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
"set -Eeuo pipefail",
|
||||
f"ACTUAL_USER={user}",
|
||||
f"PROJECT_ROOT_DIR={project_root}",
|
||||
'SUDOERS_TMP="$(mktemp)"',
|
||||
"PYTHON_PATH=$(which python3)",
|
||||
f"PROJECT_ROOT_DIR='{project_root}'",
|
||||
f"SUDOERS_TMP='{out}'",
|
||||
"SUDOERS_FILE=/etc/sudoers.d/ledmatrix_web",
|
||||
"SYSTEMCTL_PATH=/usr/bin/systemctl",
|
||||
"REBOOT_PATH=/usr/sbin/reboot",
|
||||
"POWEROFF_PATH=/usr/sbin/poweroff",
|
||||
"BASH_PATH=$(which bash)",
|
||||
"JOURNALCTL_PATH=/usr/bin/journalctl",
|
||||
block,
|
||||
f'cp "$SUDOERS_TMP" {out}',
|
||||
_step10_generation(_read(FIRST_TIME)),
|
||||
'printf %s "$SUDOERS_VALID"',
|
||||
]
|
||||
)
|
||||
subprocess.run(["bash", "-c", script], check=True)
|
||||
return subprocess.run(
|
||||
["bash", "-c", script], check=True, capture_output=True, text=True
|
||||
).stdout
|
||||
|
||||
|
||||
def _render_first_time_sudoers(tmp, user):
|
||||
"""The rules first_time_install.sh generates, via the shared library."""
|
||||
out = os.path.join(tmp, "rendered")
|
||||
assert _run_step10_generation(REPO_ROOT, user, out) == "1"
|
||||
return out
|
||||
|
||||
|
||||
def _run_step10(tmp, project_root, visudo_ok, existing=None):
|
||||
"""Run all of Step 10 against a sudoers file in `tmp`, never /etc.
|
||||
|
||||
systemctl, reboot, poweroff, journalctl and visudo are stubs, so the
|
||||
outcome does not depend on the machine running the test."""
|
||||
body = _read(FIRST_TIME)
|
||||
step = body[body.index('CURRENT_STEP="Configure passwordless sudo access"'):
|
||||
body.index('CURRENT_STEP="Configure WiFi management permissions"')]
|
||||
target = os.path.join(tmp, "ledmatrix_web")
|
||||
real = 'SUDOERS_FILE="/etc/sudoers.d/ledmatrix_web"'
|
||||
assert step.count(real) == 1
|
||||
step = step.replace(real, f"SUDOERS_FILE='{target}'")
|
||||
stubs = os.path.join(tmp, "stubs")
|
||||
os.mkdir(stubs)
|
||||
for name, code in (("systemctl", 0), ("reboot", 0), ("poweroff", 0),
|
||||
("journalctl", 0), ("visudo", 0 if visudo_ok else 1)):
|
||||
path = os.path.join(stubs, name)
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
handle.write(f"#!/bin/sh\nexit {code}\n")
|
||||
os.chmod(path, 0o755)
|
||||
if existing is not None:
|
||||
with open(target, "w", encoding="utf-8") as handle:
|
||||
handle.write(existing)
|
||||
env = dict(os.environ, TMPDIR=tmp,
|
||||
PATH=os.pathsep.join([stubs, os.path.dirname(sys.executable),
|
||||
"/usr/bin", "/bin"]))
|
||||
script = "\n".join(["set -Eeuo pipefail", "ACTUAL_USER=ledmatrix",
|
||||
f"PROJECT_ROOT_DIR='{project_root}'", step])
|
||||
result = subprocess.run(["bash", "-c", script], env=env,
|
||||
capture_output=True, text=True)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
return target, stubs, result
|
||||
|
||||
|
||||
_POSIX_STEP10 = pytest.mark.skipif(
|
||||
sys.platform == "win32" or shutil.which("which") is None,
|
||||
reason="needs a POSIX bash and which")
|
||||
|
||||
|
||||
@_POSIX_STEP10
|
||||
def test_step10_installs_the_generated_rules():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
target, stubs, _ = _run_step10(tmp, REPO_ROOT, visudo_ok=True)
|
||||
assert oct(os.stat(target).st_mode & 0o777) == "0o440"
|
||||
with open(target, encoding="utf-8") as handle:
|
||||
installed = handle.read()
|
||||
lib = os.path.join(REPO_ROOT, "scripts", "install", "lib_sudoers.sh")
|
||||
expected = subprocess.run(
|
||||
["bash", "-c", '. "$1"; web_sudoers_rules ledmatrix "$2" "$3/systemctl" '
|
||||
'"$(command -v bash)" "$3/reboot" "$3/poweroff" "$3/journalctl"',
|
||||
"_", lib, REPO_ROOT, stubs],
|
||||
check=True, capture_output=True, text=True,
|
||||
env=dict(os.environ, PATH=os.pathsep.join([stubs, "/usr/bin", "/bin"])),
|
||||
).stdout
|
||||
assert installed == expected
|
||||
assert not [f for f in os.listdir(tmp) if f.startswith("ledmatrix_web_sudoers.")]
|
||||
|
||||
|
||||
@_POSIX_STEP10
|
||||
def test_step10_without_the_library_keeps_the_existing_file():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
target, _, result = _run_step10(tmp, tmp, visudo_ok=True, existing="keep\n")
|
||||
with open(target, encoding="utf-8") as handle:
|
||||
assert handle.read() == "keep\n"
|
||||
assert "lib_sudoers.sh not found" in result.stderr
|
||||
assert "Passwordless sudo access configured" not in result.stdout
|
||||
|
||||
|
||||
@_POSIX_STEP10
|
||||
def test_step10_keeps_the_existing_file_when_the_rules_do_not_parse():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
target, _, result = _run_step10(tmp, REPO_ROOT, visudo_ok=False, existing="keep\n")
|
||||
with open(target, encoding="utf-8") as handle:
|
||||
assert handle.read() == "keep\n"
|
||||
assert "did not parse" in result.stderr
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="visudo is POSIX only")
|
||||
@pytest.mark.skipif(VISUDO is None, reason="visudo not installed")
|
||||
def test_the_rules_the_installer_emits_actually_parse():
|
||||
|
||||
Reference in New Issue
Block a user