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'); assert_true( !str_contains($mysqlServiceSource, "@\\'%\\'"), 'MySQLService must not create/grant/drop temporary phpMyAdmin SSO roles on the wildcard host (%)' ); $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)); 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";