diff --git a/exec/handlers/open_phpmyadmin.php b/exec/handlers/open_phpmyadmin.php index 796e1be..2df9b6e 100644 --- a/exec/handlers/open_phpmyadmin.php +++ b/exec/handlers/open_phpmyadmin.php @@ -58,6 +58,17 @@ return static function (AppContext $ctx): void { $ensurePhpMyAdminReady(); } catch (Throwable $e) { $message = 'Nie można przygotować phpMyAdmin: ' . $e->getMessage(); + @error_log( + sprintf( + "[%s] PHPMYADMIN_PROVISION_FAIL user=%s remote=%s message=%s\n", + date('c'), + $ctx->daUser->username(), + Http::server('REMOTE_ADDR'), + $e->getMessage() + ), + 3, + PLUGIN_ROOT . '/error.log' + ); if ($rawMode) { echo "HTTP/1.1 500 Internal Server Error\r\n"; echo "Content-Type: text/plain; charset=UTF-8\r\n\r\n"; diff --git a/exec/lib/MySQLService.php b/exec/lib/MySQLService.php index e723cd1..dc31844 100644 --- a/exec/lib/MySQLService.php +++ b/exec/lib/MySQLService.php @@ -4,6 +4,7 @@ declare(strict_types=1); final class MySQLService { private const TMP_ROLE_PREFIX = 'da_tmp_phpmyadmin_'; + public const TMP_ROLE_HOST = '127.0.0.1'; /** @var array{host:string,port:int,database:string,user:string,password:string,socket:string} */ private array $credentials; @@ -703,13 +704,14 @@ final class MySQLService public function createTemporaryRole(string $role, string $password, int $expiresAt): void { $this->query( - 'CREATE USER ' . $this->quoteLiteral($role) . '@\'%\' IDENTIFIED BY ' . $this->quoteLiteral($password) + 'CREATE USER ' . $this->quoteLiteral($role) . '@' . $this->quoteLiteral(self::TMP_ROLE_HOST) . + ' IDENTIFIED BY ' . $this->quoteLiteral($password) ); $this->query( 'INSERT INTO da_plugin_sso_accounts (role_name, host_pattern, expires_at) - VALUES (?, \'%\', ?) + VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE expires_at = VALUES(expires_at)', - [$role, (string)$expiresAt] + [$role, self::TMP_ROLE_HOST, (string)$expiresAt] ); } @@ -717,13 +719,13 @@ final class MySQLService { $this->query( 'GRANT ALL PRIVILEGES ON ' . $this->quoteIdentifier($dbName) . '.* TO ' . - $this->quoteLiteral($role) . '@\'%\'' + $this->quoteLiteral($role) . '@' . $this->quoteLiteral(self::TMP_ROLE_HOST) ); } public function dropTemporaryRole(string $role): void { - $this->tryQuery('DROP USER IF EXISTS ' . $this->quoteLiteral($role) . '@\'%\''); + $this->tryQuery('DROP USER IF EXISTS ' . $this->quoteLiteral($role) . '@' . $this->quoteLiteral(self::TMP_ROLE_HOST)); $this->query('DELETE FROM da_plugin_sso_accounts WHERE role_name = ?', [$role]); } @@ -1155,6 +1157,14 @@ final class MySQLService ); if ($ok !== true) { + error_log(sprintf( + '[%s] MYSQL_CONNECT_FAIL host=%s port=%d database=%s error=%s', + date('c'), + $this->credentials['host'], + $this->credentials['port'], + $targetDb, + $conn->connect_error ?? 'unknown' + )); throw new RuntimeException('Nie udało się połączyć z MySQL.'); } diff --git a/exec/lib/PhpMyAdminSso.php b/exec/lib/PhpMyAdminSso.php index f9da0f6..14a59cb 100644 --- a/exec/lib/PhpMyAdminSso.php +++ b/exec/lib/PhpMyAdminSso.php @@ -155,7 +155,7 @@ final class PhpMyAdminSso if (@file_put_contents($tmpPath, $encoded, LOCK_EX) === false) { throw new RuntimeException('Nie udało się zapisać ticketu phpMyAdmin.'); } - @chmod($tmpPath, 0644); + @chmod($tmpPath, 0640); if (!@rename($tmpPath, $path)) { @unlink($tmpPath); throw new RuntimeException('Nie udało się aktywować ticketu phpMyAdmin.'); diff --git a/plugin.conf b/plugin.conf index 7a894f1..2289aa5 100644 --- a/plugin.conf +++ b/plugin.conf @@ -2,7 +2,7 @@ name=alt-mysql id=alt-mysql type=user author=HITME.PL -version=1.2.11 +version=1.2.12 active=no installed=no user_run_as=root diff --git a/scripts/da-integration/alt_mysql_user_backup_compress_pre.sh b/scripts/da-integration/alt_mysql_user_backup_compress_pre.sh old mode 100644 new mode 100755 diff --git a/scripts/da-integration/alt_mysql_user_restore_post_pre_cleanup.sh b/scripts/da-integration/alt_mysql_user_restore_post_pre_cleanup.sh old mode 100644 new mode 100755 diff --git a/scripts/da-integration/block_native_database_action.sh b/scripts/da-integration/block_native_database_action.sh old mode 100644 new mode 100755 diff --git a/scripts/setup/phpmyadmin_health_check.sh b/scripts/setup/phpmyadmin_health_check.sh index f59390a..f4d0751 100644 --- a/scripts/setup/phpmyadmin_health_check.sh +++ b/scripts/setup/phpmyadmin_health_check.sh @@ -2,16 +2,23 @@ set -euo pipefail PHPMYADMIN_DIR="${PHPMYADMIN_PRIVATE_DIR:-${PHPMYADMIN_DIR:-/var/www/html/alt-mysql-phpmyadmin}}" +PHPMYADMIN_URL_PATH="${PHPMYADMIN_URL_PATH:-/alt-mysql-phpmyadmin}" CONFIG_INC="${PHPMYADMIN_DIR}/config.inc.php" SIGNON_SCRIPT="${PHPMYADMIN_DIR}/da_signon.php" SIGNON_URL_PAGE="${PHPMYADMIN_DIR}/da_login.php" RUNTIME_CONFIG="${PHPMYADMIN_DIR}/runtime/config.json" +CUSTOMBUILD_DIR="${CUSTOMBUILD_DIR:-/usr/local/directadmin/custombuild}" +APACHE_ALIAS_CUSTOM="${CUSTOMBUILD_DIR}/custom/ap2/conf/extra/httpd-alias.conf" fail() { printf 'phpMyAdmin alt-mysql health: %s\n' "$*" >&2 exit 1 } +warn() { + printf 'phpMyAdmin alt-mysql health: WARNING: %s\n' "$*" >&2 +} + [ -d "$PHPMYADMIN_DIR" ] || fail "phpMyAdmin directory not found: $PHPMYADMIN_DIR" [ -r "$PHPMYADMIN_DIR/index.php" ] || fail "phpMyAdmin index.php is not readable: $PHPMYADMIN_DIR/index.php" [ -r "$CONFIG_INC" ] || fail "config.inc.php is not readable: $CONFIG_INC" @@ -24,6 +31,13 @@ grep -Fq "da_mysql_login_consume_ticket" "$SIGNON_URL_PAGE" || fail "da_login.ph grep -Fq "DA_MYSQL_DEFAULT_CONF = '/usr/local/directadmin/conf/alt-mysql.conf'" "$SIGNON_SCRIPT" \ || fail "signon script does not default to alt-mysql.conf" +[ -r "$APACHE_ALIAS_CUSTOM" ] \ + || fail "Apache alias config not found: $APACHE_ALIAS_CUSTOM - phpMyAdmin is not reachable over HTTP, run phpmyadmin_install.sh" +grep -Fq "# BEGIN ALT_MYSQL_PHPMYADMIN" "$APACHE_ALIAS_CUSTOM" \ + || fail "Apache alias block missing in $APACHE_ALIAS_CUSTOM - phpMyAdmin is not reachable over HTTP, run phpmyadmin_install.sh" +grep -Fq "Alias ${PHPMYADMIN_URL_PATH} ${PHPMYADMIN_DIR}" "$APACHE_ALIAS_CUSTOM" \ + || fail "Apache alias in $APACHE_ALIAS_CUSTOM does not point at $PHPMYADMIN_DIR" + if [ -r "$RUNTIME_CONFIG" ] && command -v php >/dev/null 2>&1; then php -r ' $path = $argv[1]; @@ -39,4 +53,25 @@ if [ -r "$RUNTIME_CONFIG" ] && command -v php >/dev/null 2>&1; then ' "$RUNTIME_CONFIG" || fail "runtime config validation failed" fi +if [ -r "$RUNTIME_CONFIG" ] && command -v php >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then + PROBE_URL="$(php -r ' + $data = json_decode(file_get_contents($argv[1]), true); + $base = trim((string)($data["phpmyadmin_public_base_url"] ?? "")); + $path = trim((string)($data["phpmyadmin_url_path"] ?? "")); + if ($base === "" || $path === "") { + exit(0); + } + echo rtrim($base, "/") . $path . "/da_login.php"; + ' "$RUNTIME_CONFIG" 2>/dev/null || true)" + + if [ -n "$PROBE_URL" ]; then + HTTP_CODE="$(curl -s -o /dev/null -m 5 -w '%{http_code}' "$PROBE_URL" 2>/dev/null || true)" + if [ -z "$HTTP_CODE" ] || [ "$HTTP_CODE" = "000" ]; then + warn "could not reach $PROBE_URL over HTTP (no response) - verify the webserver serves the Apache alias" + elif [ "$HTTP_CODE" = "404" ]; then + warn "$PROBE_URL returned HTTP 404 - the Apache alias may not be applied yet, run: cd ${CUSTOMBUILD_DIR} && ./build rewrite_confs && systemctl reload httpd" + fi + fi +fi + echo "phpMyAdmin alt-mysql health: OK" diff --git a/scripts/setup/phpmyadmin_install.sh b/scripts/setup/phpmyadmin_install.sh index fb4c286..d5784d0 100644 --- a/scripts/setup/phpmyadmin_install.sh +++ b/scripts/setup/phpmyadmin_install.sh @@ -2,8 +2,10 @@ set -euo pipefail PHPMYADMIN_PRIVATE_DIR="${PHPMYADMIN_PRIVATE_DIR:-${PHPMYADMIN_DIR:-/var/www/html/alt-mysql-phpmyadmin}}" +PHPMYADMIN_URL_PATH="${PHPMYADMIN_URL_PATH:-/alt-mysql-phpmyadmin}" PHPMYADMIN_VERSION="${PHPMYADMIN_VERSION:-5.2.3}" PHPMYADMIN_TARBALL_URL="${PHPMYADMIN_TARBALL_URL:-https://files.phpmyadmin.net/phpMyAdmin/5.2.3/phpMyAdmin-5.2.3-all-languages.tar.gz}" +PHPMYADMIN_TARBALL_SHA256="${PHPMYADMIN_TARBALL_SHA256:-}" PHPMYADMIN_DOWNLOAD_DIR="${PHPMYADMIN_DOWNLOAD_DIR:-/usr/local/src}" PHPMYADMIN_SOURCE_DIR="${PHPMYADMIN_SOURCE_DIR:-}" RUNTIME_DIR="${PHPMYADMIN_PRIVATE_DIR}/runtime" @@ -12,6 +14,11 @@ CONFIG_INC="${PHPMYADMIN_PRIVATE_DIR}/config.inc.php" SIGNON_SCRIPT="${PHPMYADMIN_PRIVATE_DIR}/da_signon.php" SIGNON_URL_PAGE="${PHPMYADMIN_PRIVATE_DIR}/da_login.php" BLOWFISH_SECRET_FILE="${RUNTIME_DIR}/blowfish_secret" +CUSTOMBUILD_DIR="${CUSTOMBUILD_DIR:-/usr/local/directadmin/custombuild}" +CB_BUILD="${CUSTOMBUILD_DIR}/build" +APACHE_ALIAS_CUSTOM="${CUSTOMBUILD_DIR}/custom/ap2/conf/extra/httpd-alias.conf" +APACHE_ALIAS_SOURCE="/etc/httpd/conf/extra/httpd-alias.conf" +APACHE_ALIAS_CONFIGURE="${CUSTOMBUILD_DIR}/configure/ap2/conf/extra/httpd-alias.conf" if [ "${PHPMYADMIN_INSTALL_ASSUME_ROOT:-0}" != "1" ] && [ "${EUID:-$(id -u)}" -ne 0 ]; then echo "Skrypt musi być uruchomiony jako root." >&2 @@ -22,11 +29,54 @@ log() { printf 'phpMyAdmin alt-mysql: %s\n' "$*" } +warn() { + printf 'phpMyAdmin alt-mysql: WARNING: %s\n' "$*" >&2 +} + die() { printf 'phpMyAdmin alt-mysql: %s\n' "$*" >&2 exit 1 } +chown_or_warn() { + local owner="$1" errfile + shift + errfile="$(mktemp)" + chown "$owner" "$@" 2>"$errfile" || warn "chown $owner $* failed: $(cat "$errfile")" + rm -f "$errfile" +} + +chmod_or_warn() { + local mode="$1" errfile + shift + errfile="$(mktemp)" + chmod "$mode" "$@" 2>"$errfile" || warn "chmod $mode $* failed: $(cat "$errfile")" + rm -f "$errfile" +} + +detect_apache_group() { + local group_name="" + + if [ -f /etc/httpd/conf/httpd.conf ]; then + group_name="$(awk 'tolower($1)=="group" {print $2; exit}' /etc/httpd/conf/httpd.conf | tr -d '[:space:]')" + fi + + if [ -z "$group_name" ] && [ -f /etc/apache2/apache2.conf ]; then + group_name="$(awk 'tolower($1)=="group" {print $2; exit}' /etc/apache2/apache2.conf | tr -d '[:space:]')" + fi + + if [ -z "$group_name" ]; then + for candidate in apache www-data nobody; do + if getent group "$candidate" >/dev/null 2>&1; then + group_name="$candidate" + break + fi + done + fi + + printf '%s\n' "${group_name:-diradmin}" +} + install_private_copy_from_source() { local source_dir="$1" [ -d "$source_dir" ] || die "Katalog źródłowy phpMyAdmin nie istnieje: $source_dir" @@ -49,6 +99,16 @@ download_private_copy() { curl -fL --retry 3 --connect-timeout 20 -o "$tarball" "$PHPMYADMIN_TARBALL_URL" fi + if [ -n "$PHPMYADMIN_TARBALL_SHA256" ]; then + command -v sha256sum >/dev/null 2>&1 || die "Brak sha256sum do weryfikacji archiwum phpMyAdmin." + local actual_sum + actual_sum="$(sha256sum "$tarball" | awk '{print $1}')" + [ "$actual_sum" = "$PHPMYADMIN_TARBALL_SHA256" ] || { + rm -f "$tarball" + die "Niezgodna suma SHA256 archiwum phpMyAdmin. Oczekiwano: ${PHPMYADMIN_TARBALL_SHA256}, otrzymano: ${actual_sum}" + } + fi + tmp_dir="$(mktemp -d)" tar -xzf "$tarball" -C "$tmp_dir" extracted="$(find "$tmp_dir" -mindepth 1 -maxdepth 1 -type d -name 'phpMyAdmin-*' -print -quit)" @@ -474,13 +534,86 @@ function get_login_credentials($user = ''): array } PHP + local apache_group + apache_group="$(detect_apache_group)" + mkdir -p "$RUNTIME_DIR/tmp" - chown -R diradmin:diradmin "$RUNTIME_DIR" "$CONFIG_INC" "$SIGNON_SCRIPT" "$SIGNON_URL_PAGE" 2>/dev/null || true - chmod 755 "$RUNTIME_DIR" "$TICKETS_DIR" "$RUNTIME_DIR/tmp" 2>/dev/null || true - chmod 644 "$CONFIG_INC" "$SIGNON_SCRIPT" "$SIGNON_URL_PAGE" "$RUNTIME_DIR/.htaccess" "$TICKETS_DIR/.htaccess" "$RUNTIME_DIR/index.html" "$TICKETS_DIR/index.html" 2>/dev/null || true + chown_or_warn root:root "$PHPMYADMIN_PRIVATE_DIR" + chmod_or_warn 755 "$PHPMYADMIN_PRIVATE_DIR" + chown_or_warn "diradmin:${apache_group}" "$RUNTIME_DIR" "$TICKETS_DIR" "$RUNTIME_DIR/tmp" "$CONFIG_INC" "$SIGNON_SCRIPT" "$SIGNON_URL_PAGE" + chmod_or_warn 711 "$RUNTIME_DIR" "$RUNTIME_DIR/tmp" + chmod_or_warn 2733 "$TICKETS_DIR" + chmod_or_warn 644 "$CONFIG_INC" "$SIGNON_SCRIPT" "$SIGNON_URL_PAGE" "$RUNTIME_DIR/.htaccess" "$TICKETS_DIR/.htaccess" "$RUNTIME_DIR/index.html" "$TICKETS_DIR/index.html" +} + +configure_apache_alias() { + if [ ! -d "$CUSTOMBUILD_DIR" ]; then + warn "Brak katalogu CustomBuild: ${CUSTOMBUILD_DIR} - pomijam konfigurację aliasu Apache. Skonfiguruj go ręcznie: Alias ${PHPMYADMIN_URL_PATH} ${PHPMYADMIN_PRIVATE_DIR}" + return 0 + fi + + mkdir -p "$(dirname "$APACHE_ALIAS_CUSTOM")" + + if [ ! -f "$APACHE_ALIAS_CUSTOM" ]; then + if [ -f "$APACHE_ALIAS_SOURCE" ]; then + cp -p "$APACHE_ALIAS_SOURCE" "$APACHE_ALIAS_CUSTOM" + elif [ -f "$APACHE_ALIAS_CONFIGURE" ]; then + cp -p "$APACHE_ALIAS_CONFIGURE" "$APACHE_ALIAS_CUSTOM" + else + warn "Nie znaleziono źródłowego httpd-alias.conf - tworzę pusty szablon custom." + : > "$APACHE_ALIAS_CUSTOM" + fi + fi + + local tmp_file + tmp_file="$(mktemp)" + awk ' + BEGIN { skip = 0 } + /^# BEGIN ALT_MYSQL_PHPMYADMIN$/ { skip = 1; next } + /^# END ALT_MYSQL_PHPMYADMIN$/ { skip = 0; next } + skip == 0 { print } + ' "$APACHE_ALIAS_CUSTOM" > "$tmp_file" + + cat >> "$tmp_file" < + AllowOverride None + Options -Indexes + Require all granted + + + + AllowOverride None + Options -Indexes + Require all denied + +# END ALT_MYSQL_PHPMYADMIN +EOF + + mv "$tmp_file" "$APACHE_ALIAS_CUSTOM" + chmod_or_warn 644 "$APACHE_ALIAS_CUSTOM" +} + +apply_apache_alias() { + if [ -x "$CB_BUILD" ]; then + (cd "$CUSTOMBUILD_DIR" && ./build rewrite_confs) || warn "rewrite_confs zakończyło się błędem - uruchom ręcznie: cd ${CUSTOMBUILD_DIR} && ./build rewrite_confs" + else + warn "Brak ${CB_BUILD} - uruchom ręcznie: cd ${CUSTOMBUILD_DIR} && ./build rewrite_confs" + fi + + if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet httpd 2>/dev/null; then + systemctl reload httpd || warn "Nie udało się przeładować httpd." + fi } ensure_private_copy write_private_config +configure_apache_alias +if [ "${PHPMYADMIN_SKIP_APACHE_RELOAD:-0}" != "1" ]; then + apply_apache_alias +fi log "Prywatna kopia phpMyAdmin dla alt-mysql jest gotowa w ${PHPMYADMIN_PRIVATE_DIR}." diff --git a/tests/mysql_connect_failure_test.php b/tests/mysql_connect_failure_test.php new file mode 100644 index 0000000..cbd2f7a --- /dev/null +++ b/tests/mysql_connect_failure_test.php @@ -0,0 +1,62 @@ + '127.0.0.1', + 'port' => 1, + 'database' => 'mysql', + 'user' => 'nobody', + 'password' => 'nopassword', + 'socket' => '', +], $settings); + +$threw = false; +try { + $mysql->serverVersion(); +} catch (Throwable $e) { + $threw = true; +} +assert_true($threw, 'connecting to a closed port must still raise an exception'); + +$logContents = (string)file_get_contents($logPath); +assert_true( + str_contains($logContents, 'MYSQL_CONNECT_FAIL'), + 'a failed MySQL connection must be logged for diagnosis (expected MYSQL_CONNECT_FAIL marker in error log, got: ' . $logContents . ')' +); +assert_true( + str_contains($logContents, '127.0.0.1'), + 'the connect-failure log line must include the target host for diagnosis' +); + +unlink($logPath); + +echo "mysql_connect_failure_test: OK\n"; diff --git a/tests/package_archive_test.sh b/tests/package_archive_test.sh index e7943d3..0989593 100644 --- a/tests/package_archive_test.sh +++ b/tests/package_archive_test.sh @@ -2,7 +2,7 @@ set -euo pipefail PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" -ARCHIVE="${1:-$(cd "$PLUGIN_DIR/.." && pwd)/archives/1.2.11/alt-mysql.tar.gz}" +ARCHIVE="${1:-$(cd "$PLUGIN_DIR/.." && pwd)/archives/1.2.12/alt-mysql.tar.gz}" fail() { echo "FAIL: $*" >&2 @@ -37,7 +37,7 @@ fi CONF="$(tar -xOzf "$ARCHIVE" ./plugin.conf 2>/dev/null || tar -xOzf "$ARCHIVE" plugin.conf)" printf '%s\n' "$CONF" | grep -Fxq "name=alt-mysql" || fail "archive plugin name is not alt-mysql" printf '%s\n' "$CONF" | grep -Fxq "id=alt-mysql" || fail "archive plugin id is not alt-mysql" -printf '%s\n' "$CONF" | grep -Fxq "version=1.2.11" || fail "archive plugin version is not 1.2.11" +printf '%s\n' "$CONF" | grep -Fxq "version=1.2.12" || fail "archive plugin version is not 1.2.12" SETTINGS="$(tar -xOzf "$ARCHIVE" ./plugin-settings.conf 2>/dev/null || tar -xOzf "$ARCHIVE" plugin-settings.conf)" printf '%s\n' "$SETTINGS" | grep -Fxq "PHPMYADMIN_INSTALL_DIR=/var/www/html/alt-mysql-phpmyadmin" \ diff --git a/tests/phpmyadmin_install_test.sh b/tests/phpmyadmin_install_test.sh index 671cdef..0e24e7e 100644 --- a/tests/phpmyadmin_install_test.sh +++ b/tests/phpmyadmin_install_test.sh @@ -16,21 +16,19 @@ grep -Fq 'PHPMYADMIN_VERSION="${PHPMYADMIN_VERSION:-5.2.3}"' "$SCRIPT" \ grep -Fq 'PHPMYADMIN_TARBALL_URL="${PHPMYADMIN_TARBALL_URL:-https://files.phpmyadmin.net/phpMyAdmin/5.2.3/phpMyAdmin-5.2.3-all-languages.tar.gz}"' "$SCRIPT" \ || fail "phpMyAdmin tarball URL is not the explicit 5.2.3 URL" -mkdir -p "$TMP_DIR/phpMyAdmin" -cat > "$TMP_DIR/phpMyAdmin/config.inc.php" <<'PHP' - "$TMP_DIR/phpMyAdmin-source/index.php" <<'PHP' /dev/null PRIVATE_DIR="$TMP_DIR/alt-mysql-phpmyadmin" @@ -39,15 +37,27 @@ SIGNON_SCRIPT="$PRIVATE_DIR/da_signon.php" SIGNON_URL_PAGE="$PRIVATE_DIR/da_login.php" TICKET_DIR="$PRIVATE_DIR/runtime/tickets" SESSION_DIR="$TMP_DIR/sessions" +ALIAS_CONF="$CUSTOMBUILD_DIR/custom/ap2/conf/extra/httpd-alias.conf" [ -r "$PRIVATE_DIR/index.php" ] || fail "private phpMyAdmin copy was not installed" [ -r "$CONFIG_INC" ] || fail "private phpMyAdmin config was not created" [ -r "$SIGNON_SCRIPT" ] || fail "signon script was not created" [ -r "$SIGNON_URL_PAGE" ] || fail "phpMyAdmin SignonURL page was not created" [ -d "$TICKET_DIR" ] || fail "phpMyAdmin ticket directory was not created" -if grep -Fq "config.mysql.inc.php" "$TMP_DIR/phpMyAdmin/config.inc.php"; then - fail "installer modified existing CustomBuild phpMyAdmin config" -fi + +[ -r "$ALIAS_CONF" ] || fail "Apache alias config was not created" +grep -Fq "# BEGIN ALT_MYSQL_PHPMYADMIN" "$ALIAS_CONF" || fail "Apache alias block marker missing" +grep -Fq "Alias /alt-mysql-phpmyadmin ${PRIVATE_DIR}" "$ALIAS_CONF" || fail "Apache alias does not point at private phpMyAdmin dir" +grep -Fq "# END ALT_MYSQL_PHPMYADMIN" "$ALIAS_CONF" || fail "Apache alias end marker missing" + +PHPMYADMIN_PRIVATE_DIR="$TMP_DIR/alt-mysql-phpmyadmin" \ +PHPMYADMIN_SOURCE_DIR="$TMP_DIR/phpMyAdmin-source" \ +PHPMYADMIN_INSTALL_ASSUME_ROOT=1 \ +CUSTOMBUILD_DIR="$CUSTOMBUILD_DIR" \ +bash "$SCRIPT" >/dev/null + +BEGIN_COUNT="$(grep -Fc "# BEGIN ALT_MYSQL_PHPMYADMIN" "$ALIAS_CONF" || true)" +[ "$BEGIN_COUNT" -eq 1 ] || fail "Apache alias block was duplicated on re-run (found $BEGIN_COUNT times)" SERVER_JSON="$(php -r ' $_SERVER["HTTPS"] = "on"; @@ -143,7 +153,16 @@ php -d session.save_path="$SESSION_DIR" -r ' exit(3); } ' "$SIGNON_SCRIPT" || fail "signon script cannot read SSO session while phpMyAdmin session is active" -PHPMYADMIN_PRIVATE_DIR="$PRIVATE_DIR" bash "$PLUGIN_DIR/scripts/setup/phpmyadmin_health_check.sh" >/dev/null \ - || fail "phpMyAdmin health check should pass for SignonURL-capable config" +PHPMYADMIN_PRIVATE_DIR="$PRIVATE_DIR" CUSTOMBUILD_DIR="$CUSTOMBUILD_DIR" \ + bash "$PLUGIN_DIR/scripts/setup/phpmyadmin_health_check.sh" >/dev/null \ + || fail "phpMyAdmin health check should pass for SignonURL-capable config with Apache alias present" + +EMPTY_CUSTOMBUILD_DIR="$TMP_DIR/custombuild-empty" +mkdir -p "$EMPTY_CUSTOMBUILD_DIR" +HEALTH_OUTPUT="$(PHPMYADMIN_PRIVATE_DIR="$PRIVATE_DIR" CUSTOMBUILD_DIR="$EMPTY_CUSTOMBUILD_DIR" \ + bash "$PLUGIN_DIR/scripts/setup/phpmyadmin_health_check.sh" 2>&1)" \ + && fail "phpMyAdmin health check should fail when the Apache alias is missing" +echo "$HEALTH_OUTPUT" | grep -Fqi "alias" \ + || fail "phpMyAdmin health check failure message should mention the missing Apache alias (got: $HEALTH_OUTPUT)" echo "phpmyadmin_install_test: OK" diff --git a/tests/phpmyadmin_sso_test.php b/tests/phpmyadmin_sso_test.php new file mode 100644 index 0000000..aef7009 --- /dev/null +++ b/tests/phpmyadmin_sso_test.php @@ -0,0 +1,96 @@ +hasConstant('TMP_ROLE_HOST'), 'MySQLService must define a TMP_ROLE_HOST constant'); +assert_same('127.0.0.1', $mysqlServiceClass->getConstant('TMP_ROLE_HOST'), 'TMP_ROLE_HOST must scope the temporary SSO role to the phpMyAdmin app host'); + +// --- phpMyAdmin login ticket files must not be world-readable --- + +$ssoDir = sys_get_temp_dir() . '/altmysql-pma-sso-' . bin2hex(random_bytes(4)); +mkdir($ssoDir, 0755, true); + +$settings = load_settings("PHPMYADMIN_SSO_DIR={$ssoDir}\nPHPMYADMIN_TICKET_TTL=60\n"); + +$ssoClass = new ReflectionClass(PhpMyAdminSso::class); +/** @var PhpMyAdminSso $sso */ +$sso = $ssoClass->newInstanceWithoutConstructor(); +$settingsProp = $ssoClass->getProperty('settings'); +$settingsProp->setAccessible(true); +$settingsProp->setValue($sso, $settings); + +$writeTicket = $ssoClass->getMethod('writeLoginTicket'); +$writeTicket->setAccessible(true); +$auth = [ + 'user' => 'da_tmp_phpmyadmin_demo_test', + 'password' => 'secret-password', + 'host' => '127.0.0.1', + 'port' => 33033, + 'db' => 'demo_db', +]; +$token = $writeTicket->invoke($sso, $auth, 'demo_db'); + +assert_true(preg_match('/\A[a-f0-9]{64}\z/', $token) === 1, 'ticket token must be a 64 char lowercase hex string'); + +$ticketPath = $ssoDir . '/tickets/' . $token . '.json'; +assert_true(is_file($ticketPath), 'ticket file must be written to /tickets/.json'); + +$mode = fileperms($ticketPath) & 0777; +assert_same(0640, $mode, 'ticket files contain a plaintext MySQL password and must not be world-readable (expected 0640)'); + +$decoded = json_decode((string)file_get_contents($ticketPath), true); +assert_true(is_array($decoded), 'ticket file must contain valid JSON'); +assert_same('demo_db', $decoded['db'] ?? null, 'ticket must record the target database'); +assert_same('da_tmp_phpmyadmin_demo_test', $decoded['auth']['user'] ?? null, 'ticket must record the temporary role name'); + +// cleanup +array_map('unlink', glob($ssoDir . '/tickets/*') ?: []); +@rmdir($ssoDir . '/tickets'); +@rmdir($ssoDir); + +echo "phpmyadmin_sso_test: OK\n"; diff --git a/tests/plugin_identity_test.sh b/tests/plugin_identity_test.sh index e94c5f7..4decc49 100644 --- a/tests/plugin_identity_test.sh +++ b/tests/plugin_identity_test.sh @@ -14,7 +14,7 @@ case "$(basename "$PLUGIN_DIR")" in esac grep -Fxq "name=alt-mysql" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf name is not alt-mysql" grep -Fxq "id=alt-mysql" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf id is not alt-mysql" -grep -Fxq "version=1.2.11" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf version is not 1.2.11" +grep -Fxq "version=1.2.12" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf version is not 1.2.12" grep -Fq "return '/CMD_PLUGINS/alt-mysql';" "$PLUGIN_DIR/exec/lib/AppContext.php" \ || fail "AppContext base URL is not alt-mysql" grep -Fq "\$pluginId = 'alt-mysql';" "$PLUGIN_DIR/exec/lib/DirectAdminUser.php" \