mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
* fix(web): unify operation history tracking for monorepo plugin operations The operation history UI was reading from the wrong data source (operation_queue instead of operation_history), install/update records lacked version details, toggle operations used a type name that didn't match UI filters, and the Clear History button was non-functional. - Switch GET /plugins/operation/history to read from OperationHistory audit log with return type hint and targeted exception handling - Add DELETE /plugins/operation/history endpoint; wire up Clear button - Add _get_plugin_version helper with specific exception handling (FileNotFoundError, PermissionError, json.JSONDecodeError) and structured logging with plugin_id/path context - Record plugin version, branch, and commit details on install/update - Record install failures in the direct (non-queue) code path - Replace "toggle" operation type with "enable"/"disable" - Add normalizeStatus() in JS to map completed→success, error→failed so status filter works regardless of server-side convention - Truncate commit SHAs to 7 chars in details display - Fix HTML filter options, operation type colors, duplicate JS init Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(plugins): prevent root-owned files from blocking plugin updates The root ledmatrix service creates __pycache__ and data cache files owned by root inside plugin directories. The web service (non-root) cannot delete these when updating or uninstalling plugins, causing operations to fail with "Permission denied". Defense in depth with three layers: - Prevent: PYTHONDONTWRITEBYTECODE=1 in systemd service + run.py - Fallback: sudoers rules for rm on plugin directories - Code: _safe_remove_directory() now uses sudo as last resort, and all bare shutil.rmtree() calls routed through it Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): harden sudo removal with path-validated helper script Address code review findings: - Replace raw rm/find sudoers wildcards with a vetted helper script (safe_plugin_rm.sh) that resolves symlinks and validates the target is a strict child of plugin-repos/ or plugins/ before deletion - Add allow-list validation in sudo_remove_directory() that checks resolved paths against allowed bases before invoking sudo - Check _safe_remove_directory() return value before shutil.move() in the manifest ID rename path - Move stat import to module level in store_manager.py - Use stat.S_IRWXU instead of 0o777 in chmod fallback stage - Add ignore_errors=True to temp dir cleanup in finally block - Use command -v instead of which in configure_web_sudo.sh Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): address code review round 2 — harden paths and error handling - safe_plugin_rm.sh: use realpath --canonicalize-missing for ALLOWED_BASES so the script doesn't fail under set -e when dirs don't exist yet - safe_plugin_rm.sh: add -- before path in rm -rf to prevent flag injection - permission_utils.py: use shutil.which('bash') instead of hardcoded /bin/bash to match whatever path the sudoers BASH_PATH resolves to - store_manager.py: check _safe_remove_directory() return before shutil.move() in _install_from_monorepo_zip to prevent moving into a non-removed target - store_manager.py: catch OSError instead of PermissionError in Stage 1 removal to handle both EACCES and EPERM error codes - store_manager.py: hoist sudo_remove_directory import to module level - configure_web_sudo.sh: harden safe_plugin_rm.sh to root-owned 755 so the web user cannot modify the vetted helper script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): validate command paths in sudoers config and use resolved paths - configure_web_sudo.sh: validate that required commands (systemctl, bash, python3) resolve to non-empty paths before generating sudoers entries; abort with clear error if any are missing; skip optional commands (reboot, poweroff, journalctl) with a warning instead of emitting malformed NOPASSWD lines; validate helper script exists on disk - permission_utils.py: pass the already-resolved path to the subprocess call and use it for the post-removal exists() check, eliminating a TOCTOU window between Python-side validation and shell-side execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
62 lines
1.9 KiB
Bash
Executable File
62 lines
1.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# safe_plugin_rm.sh — Safely remove a plugin directory after validating
|
|
# that the resolved path is inside an allowed base directory.
|
|
#
|
|
# This script is intended to be called via sudo from the web interface.
|
|
# It prevents path traversal attacks by resolving symlinks and verifying
|
|
# the target is a child of plugin-repos/ or plugins/.
|
|
#
|
|
# Usage: safe_plugin_rm.sh <target_path>
|
|
|
|
set -euo pipefail
|
|
|
|
if [ $# -ne 1 ]; then
|
|
echo "Usage: $0 <target_path>" >&2
|
|
exit 1
|
|
fi
|
|
|
|
TARGET="$1"
|
|
|
|
# Determine the project root (parent of scripts/fix_perms/)
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
|
# Allowed base directories (resolved, no trailing slash)
|
|
# Use --canonicalize-missing so this works even if the dirs don't exist yet
|
|
ALLOWED_BASES=(
|
|
"$(realpath --canonicalize-missing "$PROJECT_ROOT/plugin-repos")"
|
|
"$(realpath --canonicalize-missing "$PROJECT_ROOT/plugins")"
|
|
)
|
|
|
|
# Resolve the target path (follow symlinks)
|
|
# Use realpath --canonicalize-missing so it works even if the path
|
|
# doesn't fully exist (e.g., partially deleted directory)
|
|
RESOLVED_TARGET="$(realpath --canonicalize-missing "$TARGET")"
|
|
|
|
# Validate: resolved target must be a strict child of an allowed base
|
|
# (must not BE the base itself — only children are allowed)
|
|
ALLOWED=false
|
|
for BASE in "${ALLOWED_BASES[@]}"; do
|
|
if [[ "$RESOLVED_TARGET" == "$BASE/"* ]]; then
|
|
ALLOWED=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ "$ALLOWED" = false ]; then
|
|
echo "DENIED: $RESOLVED_TARGET is not inside an allowed plugin directory" >&2
|
|
echo "Allowed bases: ${ALLOWED_BASES[*]}" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Safety check: refuse to delete the base directories themselves
|
|
for BASE in "${ALLOWED_BASES[@]}"; do
|
|
if [ "$RESOLVED_TARGET" = "$BASE" ]; then
|
|
echo "DENIED: cannot remove plugin base directory itself: $BASE" >&2
|
|
exit 2
|
|
fi
|
|
done
|
|
|
|
# All checks passed — remove the target
|
|
rm -rf -- "$RESOLVED_TARGET"
|