diff --git a/.docker/php/shell/entrypoint.sh b/.docker/php/shell/entrypoint.sh index 46731eb..bd1e129 100644 --- a/.docker/php/shell/entrypoint.sh +++ b/.docker/php/shell/entrypoint.sh @@ -30,4 +30,8 @@ if [ "${fresh_install}" = true ]; then akaunting_fix_ownership fi +# Repair root-owned Laravel cache directories on every start. A previous +# administrative command must not prevent the web user from writing cache. +akaunting_repair_runtime_cache_permissions + exec php-fpm diff --git a/.docker/php/shell/lib/akaunting.sh b/.docker/php/shell/lib/akaunting.sh index b40d447..480ca5c 100644 --- a/.docker/php/shell/lib/akaunting.sh +++ b/.docker/php/shell/lib/akaunting.sh @@ -96,6 +96,19 @@ akaunting_run_upgrade() { php artisan migrate --force } +# A one-off root Artisan call can create 0755 cache directories owned by root, +# while PHP-FPM writes as www-data. Restore only runtime cache ownership on +# startup. Do not traverse the full application volume or alter financial files. +akaunting_repair_runtime_cache_permissions() { + local cache_dir="${APP_ROOT}/storage/framework/cache" + if [ "$(id -u)" -ne 0 ] || [ ! -d "${cache_dir}" ]; then + return 0 + fi + + find "${cache_dir}" -xdev \( ! -user www-data -o ! -group www-data \) \ + -exec chown www-data:www-data {} + +} + akaunting_fix_ownership() { chown -R www-data:www-data "${APP_ROOT}" } diff --git a/README.md b/README.md index 0a0903f..ce46955 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,20 @@ The update command checks out the selected Akaunting tag, applies the repository If the patch no longer applies to a new upstream release, the update stops instead of continuing with a partially patched installation. +Use the `www-data` wrapper for one-off Artisan commands (do not invoke +`php artisan` as root inside the PHP container): + +```bash +bash scripts/artisan.sh about +bash scripts/artisan.sh route:list --path=api +``` + +The PHP-FPM worker writes Laravel's cache as `www-data`. Root-run Artisan +commands may create root-owned `0755` cache subdirectories, causing HTTP 500. +The container corrects ownership of **only runtime cache paths** on startup, +without recursively rewriting the application or financial data. + + ## Local development The repository intentionally does **not** version `docker-compose.override.yml`. diff --git a/scripts/artisan.sh b/scripts/artisan.sh new file mode 100755 index 0000000..1860f03 --- /dev/null +++ b/scripts/artisan.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Always run Artisan as the PHP-FPM user. Invoking it as root can create +# root-owned Laravel cache directories, making ordinary web requests fail 500. +if [ "$#" -eq 0 ]; then + echo "Usage: scripts/artisan.sh [arguments...]" >&2 + exit 2 +fi +service="${AKAUNTING_SERVICE:-akaunting.php}" +docker compose exec -u www-data "${service}" php artisan "$@" diff --git a/tests/artisan.bats b/tests/artisan.bats new file mode 100644 index 0000000..723a444 --- /dev/null +++ b/tests/artisan.bats @@ -0,0 +1,27 @@ +#!/usr/bin/env bats + +load test_helper + +setup() { + setup_test_bin + stub_command docker +} + +@test "artisan wrapper executes as the same user as PHP-FPM" { + run bash "${BATS_TEST_DIRNAME}/../scripts/artisan.sh" cache:clear + [ "$status" -eq 0 ] + grep -q '^docker compose exec -u www-data akaunting.php php artisan cache:clear$' "${CALL_LOG}" +} + +@test "artisan wrapper preserves arguments and selected service" { + AKAUNTING_SERVICE=custom.php run bash "${BATS_TEST_DIRNAME}/../scripts/artisan.sh" route:list --path=api + [ "$status" -eq 0 ] + grep -q '^docker compose exec -u www-data custom.php php artisan route:list --path=api$' "${CALL_LOG}" +} + +@test "artisan wrapper refuses empty commands" { + run bash "${BATS_TEST_DIRNAME}/../scripts/artisan.sh" + [ "$status" -eq 2 ] + [[ "$output" == *"Usage:"* ]] + [ ! -s "${CALL_LOG}" ] +} diff --git a/tests/entrypoint.bats b/tests/entrypoint.bats index 6f10366..4ea2a57 100644 --- a/tests/entrypoint.bats +++ b/tests/entrypoint.bats @@ -24,7 +24,7 @@ setup() { run bash "${BATS_TEST_DIRNAME}/../.docker/php/shell/entrypoint.sh" [ "$status" -eq 0 ] - [ "$(cat "${TEST_LIFECYCLE_LOG}")" = "patches" ] + [ "$(cat "${TEST_LIFECYCLE_LOG}")" = $'patches\ncache-ownership' ] grep -q '^php-fpm ' "${CALL_LOG}" } @@ -32,7 +32,7 @@ setup() { run bash "${BATS_TEST_DIRNAME}/../.docker/php/shell/entrypoint.sh" [ "$status" -eq 0 ] - expected=$'clone\npatches\nphp-dependencies\ninstall\nownership' + expected=$'clone\npatches\nphp-dependencies\ninstall\nownership\ncache-ownership' [ "$(cat "${TEST_LIFECYCLE_LOG}")" = "$expected" ] } @@ -42,7 +42,7 @@ setup() { run bash "${BATS_TEST_DIRNAME}/../.docker/php/shell/entrypoint.sh" [ "$status" -eq 0 ] - expected=$'patches\nphp-dependencies' + expected=$'patches\nphp-dependencies\ncache-ownership' [ "$(cat "${TEST_LIFECYCLE_LOG}")" = "$expected" ] } @@ -54,6 +54,6 @@ setup() { run bash "${BATS_TEST_DIRNAME}/../.docker/php/shell/entrypoint.sh" [ "$status" -eq 0 ] - expected=$'patches\ninstall' + expected=$'patches\ninstall\ncache-ownership' [ "$(cat "${TEST_LIFECYCLE_LOG}")" = "$expected" ] } diff --git a/tests/test_helper.bash b/tests/test_helper.bash index 620de3d..08c7616 100644 --- a/tests/test_helper.bash +++ b/tests/test_helper.bash @@ -55,6 +55,10 @@ akaunting_fix_ownership() { record_lifecycle ownership } +akaunting_repair_runtime_cache_permissions() { + record_lifecycle cache-ownership +} + akaunting_update_checkout() { record_lifecycle checkout }