9a5781e5ed
The top-nav PHPMYADMIN button intentionally omits adminer_db to request access to every database. determineGrantTargets() and the ticket's new restrict_db field now distinguish that case from a specific per-row database request instead of collapsing both into single-database scope.
143 lines
5.9 KiB
PHP
143 lines
5.9 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
$pluginDir = dirname(__DIR__);
|
|
require_once $pluginDir . '/exec/lib/Settings.php';
|
|
require_once $pluginDir . '/exec/lib/MySQLService.php';
|
|
require_once $pluginDir . '/exec/lib/PhpMyAdminSso.php';
|
|
|
|
function fail(string $message): void
|
|
{
|
|
fwrite(STDERR, "FAIL: {$message}\n");
|
|
exit(1);
|
|
}
|
|
|
|
function assert_true(bool $condition, string $message): void
|
|
{
|
|
if (!$condition) {
|
|
fail($message);
|
|
}
|
|
}
|
|
|
|
function assert_same($expected, $actual, string $message): void
|
|
{
|
|
if ($expected !== $actual) {
|
|
fail($message . ' expected=' . var_export($expected, true) . ' actual=' . var_export($actual, true));
|
|
}
|
|
}
|
|
|
|
function load_settings(string $contents): Settings
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'altmysql-settings-');
|
|
if ($path === false) {
|
|
fail('cannot create temp settings file');
|
|
}
|
|
file_put_contents($path, $contents);
|
|
$settings = Settings::load($path);
|
|
unlink($path);
|
|
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');
|
|
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 ---
|
|
// --- unless no specific database was requested at all (the top-nav PHPMYADMIN button), which must grant everything ---
|
|
|
|
$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']),
|
|
'requesting 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 requested database that is not in the owned list must never be granted'
|
|
);
|
|
assert_same(
|
|
['demo_target', 'demo_other'],
|
|
$determineGrantTargets->invoke(null, null, ['demo_target', 'demo_other']),
|
|
'no database requested at all (top-nav PHPMYADMIN button) must grant every owned database'
|
|
);
|
|
assert_same(
|
|
['demo_target', 'demo_other'],
|
|
$determineGrantTargets->invoke(null, '', ['demo_target', 'demo_other']),
|
|
'an empty-string requested database must also grant every owned database'
|
|
);
|
|
|
|
// --- 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', '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 <sso_dir>/tickets/<token>.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/landing database');
|
|
assert_same('demo_db', $decoded['restrict_db'] ?? null, 'a single-database login must record that database as the grant restriction');
|
|
assert_same('da_tmp_phpmyadmin_demo_test', $decoded['auth']['user'] ?? null, 'ticket must record the temporary role name');
|
|
|
|
$allDbToken = $writeTicket->invoke($sso, $auth, 'demo_db', '');
|
|
$allDbTicketPath = $ssoDir . '/tickets/' . $allDbToken . '.json';
|
|
$allDbDecoded = json_decode((string)file_get_contents($allDbTicketPath), true);
|
|
assert_same('', $allDbDecoded['restrict_db'] ?? null, 'an all-databases login must record an empty restrict_db (no restriction)');
|
|
|
|
// cleanup
|
|
array_map('unlink', glob($ssoDir . '/tickets/*') ?: []);
|
|
@rmdir($ssoDir . '/tickets');
|
|
@rmdir($ssoDir);
|
|
|
|
echo "phpmyadmin_sso_test: OK\n";
|