diff --git a/exec/handlers/index.php b/exec/handlers/index.php index a2e7668..7c9a9b8 100644 --- a/exec/handlers/index.php +++ b/exec/handlers/index.php @@ -320,6 +320,19 @@ return static function (AppContext $ctx): void { $generatedRole = $selectedRole; $generatedDb = $dbName; $ok[] = 'Baza danych została utworzona.'; + + // Reset the create-database form so the next database can be + // created without manually clearing the previous input. + $_POST['db_suffix'] = ''; + $_POST['db_name'] = ''; + $_POST['role_name'] = ''; + $_POST['password'] = ''; + $_POST['existing_role'] = ''; + $_POST['role_mode'] = 'new'; + $_POST['creation_mode'] = 'simple'; + $_POST['remote_host'] = ''; + $_POST['remote_comment'] = ''; + $advancedPosted = false; } catch (Throwable $inner) { if ($createdDatabase) { try { diff --git a/exec/lib/PhpMyAdminRuntimeConfig.php b/exec/lib/PhpMyAdminRuntimeConfig.php index 5576c64..f9632a5 100644 --- a/exec/lib/PhpMyAdminRuntimeConfig.php +++ b/exec/lib/PhpMyAdminRuntimeConfig.php @@ -25,6 +25,7 @@ final class PhpMyAdminRuntimeConfig 'host' => 'dynamic:' . basename($settings->mysqlCredentialsFile()), 'credentials_file' => $settings->mysqlCredentialsFile(), 'client_bin_dir' => $settings->mysqlClientBinDir(), + 'directadmin_panel_port' => $settings->directAdminPanelPort(), 'updated_at' => time(), ]; diff --git a/exec/lib/PhpMyAdminSso.php b/exec/lib/PhpMyAdminSso.php index 14a59cb..1870056 100644 --- a/exec/lib/PhpMyAdminSso.php +++ b/exec/lib/PhpMyAdminSso.php @@ -60,7 +60,7 @@ final class PhpMyAdminSso $this->db->createTemporaryRole($role, $password, $expiresAt); try { - foreach ($databaseNames as $dbName) { + foreach (self::determineGrantTargets($targetDatabase, $databaseNames) as $dbName) { $this->db->grantTemporaryAdminerAccess($dbName, $role); } } catch (Throwable $e) { @@ -86,6 +86,15 @@ final class PhpMyAdminSso ]; } + /** + * @param string[] $ownedDatabases + * @return string[] + */ + private static function determineGrantTargets(string $targetDatabase, array $ownedDatabases): array + { + return in_array($targetDatabase, $ownedDatabases, true) ? [$targetDatabase] : []; + } + private function buildPhpMyAdminLoginUrl(string $database, string $ticket): string { $query = http_build_query([ diff --git a/exec/lib/Settings.php b/exec/lib/Settings.php index b2a0734..875cc5a 100644 --- a/exec/lib/Settings.php +++ b/exec/lib/Settings.php @@ -309,6 +309,12 @@ final class Settings return $ttl; } + public function directAdminPanelPort(): string + { + $port = trim($this->getString('DIRECTADMIN_PANEL_PORT', '2222')); + return ctype_digit($port) && $port !== '' ? $port : '2222'; + } + public function phpMyAdminIntegrationWarning(): string { if (!$this->getBool('ENABLE_PHPMYADMIN', $this->getBool('ENABLE_ADMINER', true))) { diff --git a/plugin-settings.conf b/plugin-settings.conf index 5c4a005..81d87ac 100644 --- a/plugin-settings.conf +++ b/plugin-settings.conf @@ -65,6 +65,8 @@ PHPMYADMIN_TICKET_TTL=120 PHPMYADMIN_SESSION_TTL=900 # PHPMYADMIN_ROLE_TTL - ważność tymczasowego użytkownika MySQL dla sesji phpMyAdmin (sekundy). PHPMYADMIN_ROLE_TTL=1200 +# DIRECTADMIN_PANEL_PORT - port panelu DirectAdmin, na który phpMyAdmin wraca po wylogowaniu. +DIRECTADMIN_PANEL_PORT=2222 # ROUNDCUBE_* - opcjonalna integracja Roundcube z alt-mariadb; migrację uruchamia się ręcznie skryptem roundcube_migrate_to_alt_mysql.sh. ROUNDCUBE_ENABLED=true ROUNDCUBE_DB_NAME=roundcube diff --git a/plugin.conf b/plugin.conf index ad8348a..de46a9a 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.13 +version=1.2.14 active=no installed=no user_run_as=root diff --git a/scripts/setup/phpmyadmin_install.sh b/scripts/setup/phpmyadmin_install.sh index 1dc9cac..1d80d1f 100644 --- a/scripts/setup/phpmyadmin_install.sh +++ b/scripts/setup/phpmyadmin_install.sh @@ -308,6 +308,58 @@ function da_mysql_phpmyadmin_signon_url(array $runtimeConfig): string . '/da_login.php'; } +function da_mysql_phpmyadmin_panel_logout_url(array $runtimeConfig): string +{ + $scheme = 'http'; + $https = strtolower((string)($_SERVER['HTTPS'] ?? '')); + if ($https !== '' && $https !== 'off' && $https !== '0') { + $scheme = 'https'; + } elseif (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https') { + $scheme = 'https'; + } elseif (strtolower((string)($_SERVER['REQUEST_SCHEME'] ?? '')) === 'https') { + $scheme = 'https'; + } + + $host = trim((string)($_SERVER['HTTP_HOST'] ?? '')); + if ($host === '') { + $host = trim((string)($_SERVER['SERVER_NAME'] ?? 'localhost')); + } + if ($host === '') { + $host = 'localhost'; + } + $host = preg_replace('/:\d+$/', '', $host) ?? $host; + + $port = trim((string)($runtimeConfig['directadmin_panel_port'] ?? '2222')); + if ($port === '' || !ctype_digit($port)) { + $port = '2222'; + } + + return $scheme . '://' . $host . ':' . $port . '/CMD_PLUGINS/alt-mysql/index.html'; +} + +function da_mysql_phpmyadmin_only_db(): string +{ + $sessionName = 'DA_MYSQL_PHPMYADMIN'; + + if (session_status() === PHP_SESSION_ACTIVE) { + return ''; + } + + $sessionId = (string)($_COOKIE[$sessionName] ?? ''); + if ($sessionId === '' || preg_match('/\A[A-Za-z0-9,-]+\z/', $sessionId) !== 1) { + return ''; + } + + session_name($sessionName); + session_id($sessionId); + session_start(); + $auth = $_SESSION['DA_MYSQL_PHPMYADMIN_AUTH'] ?? []; + $db = is_array($auth) ? (string)($auth['db'] ?? '') : ''; + session_write_close(); + + return $db; +} + $runtimeConfig = da_mysql_phpmyadmin_runtime_config(); $credentialsFile = (string)($runtimeConfig['credentials_file'] ?? DA_MYSQL_PMA_DEFAULT_CONF); if ($credentialsFile === '') { @@ -329,10 +381,10 @@ $cfg['Servers'][$i]['auth_type'] = 'signon'; $cfg['Servers'][$i]['SignonScript'] = __DIR__ . '/da_signon.php'; $cfg['Servers'][$i]['SignonSession'] = 'DA_MYSQL_PHPMYADMIN'; $cfg['Servers'][$i]['SignonURL'] = da_mysql_phpmyadmin_signon_url($runtimeConfig); -$cfg['Servers'][$i]['LogoutURL'] = da_mysql_phpmyadmin_signon_url($runtimeConfig); +$cfg['Servers'][$i]['LogoutURL'] = da_mysql_phpmyadmin_panel_logout_url($runtimeConfig); $cfg['Servers'][$i]['AllowNoPassword'] = false; $cfg['Servers'][$i]['verbose'] = 'DirectAdmin alt-mysql'; -$cfg['Servers'][$i]['only_db'] = ''; +$cfg['Servers'][$i]['only_db'] = da_mysql_phpmyadmin_only_db(); $cfg['ServerDefault'] = 1; $cfg['AllowArbitraryServer'] = false; PHP diff --git a/tests/package_archive_test.sh b/tests/package_archive_test.sh index 32a0c76..beaabe3 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.13/alt-mysql.tar.gz}" +ARCHIVE="${1:-$(cd "$PLUGIN_DIR/.." && pwd)/archives/1.2.14/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.13" || fail "archive plugin version is not 1.2.13" +printf '%s\n' "$CONF" | grep -Fxq "version=1.2.14" || fail "archive plugin version is not 1.2.14" 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 55a112b..1e2f849 100644 --- a/tests/phpmyadmin_install_test.sh +++ b/tests/phpmyadmin_install_test.sh @@ -116,11 +116,30 @@ php -r ' fwrite(STDERR, "SignonURL mismatch\n"); exit(5); } - if (($server["LogoutURL"] ?? "") !== "https://panel.example.test/alt-mysql-phpmyadmin/da_login.php") { - fwrite(STDERR, "LogoutURL mismatch\n"); + if (($server["LogoutURL"] ?? "") !== "https://panel.example.test:2222/CMD_PLUGINS/alt-mysql/index.html") { + fwrite(STDERR, "LogoutURL mismatch, got: " . ($server["LogoutURL"] ?? "(empty)") . "\n"); exit(6); } ' "$SERVER_JSON" || fail "phpMyAdmin config does not target alt-mariadb TCP endpoint" + +# config.inc.php must restrict phpMyAdmin's visible database to the one the +# SSO ticket was issued for, not show every database the account can reach. +mkdir -p "$SESSION_DIR" +ONLY_DB="$(php -d session.save_path="$SESSION_DIR" -r ' + session_name("DA_MYSQL_PHPMYADMIN"); + session_id("onlydbtest"); + session_start(); + $_SESSION["DA_MYSQL_PHPMYADMIN_AUTH"] = ["user" => "x", "password" => "y", "db" => "demo_target_db"]; + session_write_close(); + + $_COOKIE["DA_MYSQL_PHPMYADMIN"] = "onlydbtest"; + $cfg = []; + include $argv[1]; + echo $cfg["Servers"][1]["only_db"] ?? ""; +' "$CONFIG_INC")" +[ "$ONLY_DB" = "demo_target_db" ] \ + || fail "config.inc.php only_db should be scoped to the SSO ticket's database, got: '$ONLY_DB'" + grep -Fq 'return [$username, $password];' "$SIGNON_SCRIPT" \ || fail "signon script should return username/password only" php -d display_errors=1 -r 'set_error_handler(static function($errno, $errstr) { fwrite(STDERR, $errstr . "\n"); exit(9); }); $cfg=[]; include $argv[1]; include $argv[2];' "$CONFIG_INC" "$SIGNON_SCRIPT" \ diff --git a/tests/phpmyadmin_sso_test.php b/tests/phpmyadmin_sso_test.php index aef7009..d10b631 100644 --- a/tests/phpmyadmin_sso_test.php +++ b/tests/phpmyadmin_sso_test.php @@ -38,6 +38,17 @@ function load_settings(string $contents): Settings return $settings; } +// --- phpMyAdmin logout must know the DirectAdmin panel port to build a valid return URL --- + +$defaultPanelPortSettings = load_settings(''); +assert_same('2222', $defaultPanelPortSettings->directAdminPanelPort(), 'DIRECTADMIN_PANEL_PORT should default to 2222'); + +$customPanelPortSettings = load_settings("DIRECTADMIN_PANEL_PORT=2280\n"); +assert_same('2280', $customPanelPortSettings->directAdminPanelPort(), 'DIRECTADMIN_PANEL_PORT should be configurable'); + +$invalidPanelPortSettings = load_settings("DIRECTADMIN_PANEL_PORT=not-a-port\n"); +assert_same('2222', $invalidPanelPortSettings->directAdminPanelPort(), 'a non-numeric DIRECTADMIN_PANEL_PORT should fall back to 2222'); + // --- Temporary SSO role must be host-scoped, not wildcard '%' --- $mysqlServiceSource = (string)file_get_contents($pluginDir . '/exec/lib/MySQLService.php'); @@ -50,6 +61,24 @@ $mysqlServiceClass = new ReflectionClass(MySQLService::class); assert_true($mysqlServiceClass->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 SSO must only grant access to the database being logged into, not every database the account owns --- + +$ssoClassForGrants = new ReflectionClass(PhpMyAdminSso::class); +assert_true($ssoClassForGrants->hasMethod('determineGrantTargets'), 'PhpMyAdminSso must expose a determineGrantTargets() decision method'); +$determineGrantTargets = $ssoClassForGrants->getMethod('determineGrantTargets'); +$determineGrantTargets->setAccessible(true); + +assert_same( + ['demo_target'], + $determineGrantTargets->invoke(null, 'demo_target', ['demo_target', 'demo_other']), + 'issuing a ticket for one database must only grant that database, not every database the user owns' +); +assert_same( + [], + $determineGrantTargets->invoke(null, 'not_owned', ['demo_target', 'demo_other']), + 'a target database that is not in the owned list must never be granted' +); + // --- phpMyAdmin login ticket files must not be world-readable --- $ssoDir = sys_get_temp_dir() . '/altmysql-pma-sso-' . bin2hex(random_bytes(4)); diff --git a/tests/plugin_identity_test.sh b/tests/plugin_identity_test.sh index 236d500..be4e7bd 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.13" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf version is not 1.2.13" +grep -Fxq "version=1.2.14" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf version is not 1.2.14" 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" \