diff --git a/.docker/wordpress/entrypoint.sh b/.docker/wordpress/entrypoint.sh index b596548..880169b 100644 --- a/.docker/wordpress/entrypoint.sh +++ b/.docker/wordpress/entrypoint.sh @@ -48,6 +48,17 @@ is_truthy() { esac } +is_local_environment() { + case "${WP_ENVIRONMENT_TYPE:-}" in + local|development) + return 0 + ;; + *) + return 1 + ;; + esac +} + plugins_config_exists() { [ -n "$(trim_value "${WORDPRESS_SETUP_CONFIG_YAML:-}")" ] } @@ -127,6 +138,11 @@ sync_site_urls() { return fi + if ! is_local_environment; then + echo "ERROR: PROD_SITE_URL is set but WP_ENVIRONMENT_TYPE is not local or development; skipping URL synchronization." >&2 + return + fi + prod_url="$(normalize_url "${PROD_SITE_URL}")" local_url="$(normalize_url "${LOCAL_SITE_URL:-http://localhost}")" prod_host="$(strip_protocol "${prod_url}")" @@ -151,6 +167,11 @@ reset_local_user_passwords() { return fi + if ! is_local_environment; then + echo "ERROR: WORDPRESS_LOCAL_USERS_PASSWORD is set but WP_ENVIRONMENT_TYPE is not local or development; skipping password reset." >&2 + return + fi + if is_truthy "$reset_all_users"; then echo "Resetting password for all local WordPress users..." while IFS= read -r user_id; do diff --git a/README.md b/README.md index a3fcfaa..76b3275 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ services: wordpress: environment: XDEBUG_MODE: ${XDEBUG_MODE:-debug} + # PROD_SITE_URL and the password reset below only run when this is local or development. + WP_ENVIRONMENT_TYPE: local # Production base URL used for automatic search-replace in the database. # Set this only when importing a production dump and needing URL remap. PROD_SITE_URL: ${PROD_SITE_URL:-} @@ -104,7 +106,7 @@ After that, start the stack with the standard Compose command: docker compose up -d --build ``` -On each startup (when WordPress is already installed), the entrypoint can automatically reset local user passwords: +On each startup (when WordPress is already installed and `WP_ENVIRONMENT_TYPE` is `local` or `development`), the entrypoint can automatically reset local user passwords: - `WORDPRESS_LOCAL_RESET_ALL_USERS_PASSWORDS=1`: reset all users to `WORDPRESS_LOCAL_USERS_PASSWORD`. - `WORDPRESS_LOCAL_RESET_PASSWORD_FOR_USER=`: reset only one user. diff --git a/common-services.yml b/common-services.yml index 1d45bfc..e9e7c5f 100644 --- a/common-services.yml +++ b/common-services.yml @@ -16,6 +16,7 @@ services: - WORDPRESS_DB_PASSWORD=${WORDPRESS_DB_PASSWORD:-root} - WORDPRESS_TABLE_PREFIX=${WORDPRESS_TABLE_PREFIX:-wp_} - WORDPRESS_SETUP_CONFIG_YAML=${WORDPRESS_SETUP_CONFIG_YAML:-} + - WP_ENVIRONMENT_TYPE - WORDPRESS_LOCAL_USERS_PASSWORD=${WORDPRESS_LOCAL_USERS_PASSWORD:-} - WORDPRESS_LOCAL_RESET_ALL_USERS_PASSWORDS=${WORDPRESS_LOCAL_RESET_ALL_USERS_PASSWORDS:-0} - WORDPRESS_LOCAL_RESET_PASSWORD_FOR_USER=${WORDPRESS_LOCAL_RESET_PASSWORD_FOR_USER:-} diff --git a/tests/security/helpers/entrypoint.bash b/tests/security/helpers/entrypoint.bash new file mode 100644 index 0000000..43ded7f --- /dev/null +++ b/tests/security/helpers/entrypoint.bash @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +entrypoint_root="${BATS_TEST_DIRNAME}/../.." +entrypoint_tmp="${BATS_TEST_TMPDIR}/entrypoint" +entrypoint_network="" +entrypoint_database_container="" + +entrypoint_setup() { + entrypoint_network="entrypoint-bats-${BATS_TEST_NUMBER:-0}-$$" + mkdir -p "${entrypoint_tmp}/document-root" "${entrypoint_tmp}/bin" "${entrypoint_tmp}/log" + touch "${entrypoint_tmp}/document-root/wp-load.php" "${entrypoint_tmp}/document-root/wp-config.php" + chmod 777 "${entrypoint_tmp}/log" + + cat > "${entrypoint_tmp}/bin/wp" <<'WP_STUB' +#!/bin/sh +echo "wp $*" >> /var/log/entrypoint/wp-calls +case "$1 $2" in + "user list") echo 1 ;; + "user get") echo 1 ;; +esac +exit 0 +WP_STUB + printf '%s\n' '#!/bin/sh' 'exit 0' > "${entrypoint_tmp}/bin/php-fpm" + chmod 755 "${entrypoint_tmp}/bin/wp" "${entrypoint_tmp}/bin/php-fpm" + + printf '%s\n' 'server { listen 3306; return 444; }' > "${entrypoint_tmp}/database.conf" + docker network create "${entrypoint_network}" >/dev/null + entrypoint_database_container="$(docker run -d --rm \ + --network "${entrypoint_network}" --network-alias mariadb \ + -v "${entrypoint_tmp}/database.conf:/etc/nginx/conf.d/default.conf:ro" \ + nginx:latest)" +} + +entrypoint_teardown() { + if [ -n "${entrypoint_database_container}" ]; then + docker rm -f "${entrypoint_database_container}" >/dev/null 2>&1 || true + entrypoint_database_container="" + fi + if [ -n "${entrypoint_network}" ]; then + docker network rm "${entrypoint_network}" >/dev/null 2>&1 || true + entrypoint_network="" + fi +} + +entrypoint_run() { + local -a environment_args=() + local variable + + for variable in "$@"; do + environment_args+=(-e "${variable}") + done + + docker run --rm \ + --network "${entrypoint_network}" \ + -e "HOST_UID=$(id -u)" -e "HOST_GID=$(id -g)" \ + "${environment_args[@]}" \ + -v "${entrypoint_root}/.docker/wordpress/entrypoint.sh:/var/www/scripts/entrypoint.sh:ro" \ + -v "${entrypoint_tmp}/bin/wp:/usr/local/bin/wp:ro" \ + -v "${entrypoint_tmp}/bin/php-fpm:/usr/local/sbin/php-fpm:ro" \ + -v "${entrypoint_tmp}/log:/var/log/entrypoint" \ + -v "${entrypoint_tmp}/document-root:/var/www/html" \ + php:8.3-fpm bash /var/www/scripts/entrypoint.sh +} + +entrypoint_wp_calls() { + cat "${entrypoint_tmp}/log/wp-calls" 2>/dev/null || true +} diff --git a/tests/security/local-only-setup.bats b/tests/security/local-only-setup.bats new file mode 100644 index 0000000..5ed858d --- /dev/null +++ b/tests/security/local-only-setup.bats @@ -0,0 +1,43 @@ +#!/usr/bin/env bats + +load 'helpers/entrypoint.bash' + +setup() { + entrypoint_setup +} + +teardown() { + entrypoint_teardown +} + +@test "password reset is skipped when the environment type is not set" { + run entrypoint_run WORDPRESS_LOCAL_USERS_PASSWORD=admin WORDPRESS_LOCAL_RESET_ALL_USERS_PASSWORDS=1 + [ "${status}" -eq 0 ] + [[ "${output}" == *"ERROR: WORDPRESS_LOCAL_USERS_PASSWORD"* ]] + [[ "$(entrypoint_wp_calls)" != *"user update"* ]] +} + +@test "password reset is skipped in production" { + run entrypoint_run WP_ENVIRONMENT_TYPE=production WORDPRESS_LOCAL_USERS_PASSWORD=admin WORDPRESS_LOCAL_RESET_PASSWORD_FOR_USER=admin + [ "${status}" -eq 0 ] + [[ "$(entrypoint_wp_calls)" != *"user update"* ]] +} + +@test "password reset runs in a local environment" { + run entrypoint_run WP_ENVIRONMENT_TYPE=local WORDPRESS_LOCAL_USERS_PASSWORD=admin WORDPRESS_LOCAL_RESET_ALL_USERS_PASSWORDS=1 + [ "${status}" -eq 0 ] + [[ "$(entrypoint_wp_calls)" == *"user update 1 --user_pass=admin"* ]] +} + +@test "URL synchronization is skipped outside local environments" { + run entrypoint_run WP_ENVIRONMENT_TYPE=staging PROD_SITE_URL=https://example.com + [ "${status}" -eq 0 ] + [[ "${output}" == *"ERROR: PROD_SITE_URL"* ]] + [[ "$(entrypoint_wp_calls)" != *"search-replace"* ]] +} + +@test "URL synchronization runs in a development environment" { + run entrypoint_run WP_ENVIRONMENT_TYPE=development PROD_SITE_URL=https://example.com + [ "${status}" -eq 0 ] + [[ "$(entrypoint_wp_calls)" == *"search-replace https://example.com http://localhost"* ]] +}