217 lines
7.4 KiB
PHP
217 lines
7.4 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
final class PhpMyAdminSso
|
|
{
|
|
private Settings $settings;
|
|
private DirectAdminUser $daUser;
|
|
private MySQLService $db;
|
|
|
|
public function __construct(Settings $settings, DirectAdminUser $daUser, MySQLService $db)
|
|
{
|
|
$this->settings = $settings;
|
|
$this->daUser = $daUser;
|
|
$this->db = $db;
|
|
}
|
|
|
|
/**
|
|
* @return array{target:string,auth:array<string,string|int>}
|
|
*/
|
|
public function issueLoginPayload(?string $requestedDatabase = null): array
|
|
{
|
|
if (!$this->settings->enableAdminer()) {
|
|
throw new RuntimeException('Integracja phpMyAdmin jest wyłączona.');
|
|
}
|
|
|
|
$this->db->cleanupExpiredAdminerRoles();
|
|
|
|
$databaseRows = $this->db->listDatabasesForUser($this->daUser->username(), false);
|
|
if (empty($databaseRows)) {
|
|
throw new RuntimeException('Użytkownik nie ma żadnej bazy MySQL do otwarcia w phpMyAdmin.');
|
|
}
|
|
|
|
$databaseNames = [];
|
|
foreach ($databaseRows as $row) {
|
|
$name = (string)($row['name'] ?? '');
|
|
if ($name !== '') {
|
|
$databaseNames[] = $name;
|
|
}
|
|
}
|
|
$databaseNames = array_values(array_unique($databaseNames));
|
|
|
|
$targetDatabase = '';
|
|
if ($requestedDatabase !== null) {
|
|
$requestedDatabase = trim($requestedDatabase);
|
|
if ($requestedDatabase !== '') {
|
|
if (!in_array($requestedDatabase, $databaseNames, true)) {
|
|
throw new RuntimeException('Wskazana baza danych nie należy do zalogowanego użytkownika.');
|
|
}
|
|
$targetDatabase = $requestedDatabase;
|
|
}
|
|
}
|
|
if ($targetDatabase === '') {
|
|
$targetDatabase = $databaseNames[0];
|
|
}
|
|
|
|
$roleTtl = max($this->settings->adminerRoleTtl(), $this->settings->adminerSessionTtl() + 120);
|
|
$role = $this->generateTemporaryRoleName();
|
|
$password = $this->generateSecret(32);
|
|
$expiresAt = time() + $roleTtl;
|
|
|
|
$this->db->createTemporaryRole($role, $password, $expiresAt);
|
|
try {
|
|
foreach ($databaseNames as $dbName) {
|
|
$this->db->grantTemporaryAdminerAccess($dbName, $role);
|
|
}
|
|
} catch (Throwable $e) {
|
|
try {
|
|
$this->db->dropTemporaryRole($role);
|
|
} catch (Throwable $ignored) {
|
|
}
|
|
throw new RuntimeException('Nie udało się przygotować sesji phpMyAdmin: ' . $e->getMessage(), 0, $e);
|
|
}
|
|
|
|
$auth = [
|
|
'user' => $role,
|
|
'password' => $password,
|
|
'host' => $this->db->connectionEndpoint()['host'],
|
|
'port' => $this->db->connectionEndpoint()['port'],
|
|
'db' => $targetDatabase,
|
|
];
|
|
$ticket = $this->writeLoginTicket($auth, $targetDatabase);
|
|
|
|
return [
|
|
'target' => $this->buildPhpMyAdminLoginUrl($targetDatabase, $ticket),
|
|
'auth' => $auth,
|
|
];
|
|
}
|
|
|
|
private function buildPhpMyAdminLoginUrl(string $database, string $ticket): string
|
|
{
|
|
$query = http_build_query([
|
|
'ticket' => $ticket,
|
|
'route' => '/database/structure',
|
|
'db' => $database,
|
|
]);
|
|
|
|
return $this->phpMyAdminBaseUrl() . '/da_login.php?' . $query;
|
|
}
|
|
|
|
private function phpMyAdminBaseUrl(): string
|
|
{
|
|
$path = $this->settings->adminerUrlPath();
|
|
$customBase = $this->settings->adminerPublicBaseUrl();
|
|
if ($customBase !== '') {
|
|
return $customBase . $path;
|
|
}
|
|
|
|
$scheme = 'http';
|
|
$https = strtolower(Http::server('HTTPS'));
|
|
if ($https !== '' && $https !== 'off' && $https !== '0') {
|
|
$scheme = 'https';
|
|
} elseif (strtolower(Http::server('HTTP_X_FORWARDED_PROTO')) === 'https') {
|
|
$scheme = 'https';
|
|
} elseif (strtolower(Http::server('REQUEST_SCHEME')) === 'https') {
|
|
$scheme = 'https';
|
|
}
|
|
|
|
$host = Http::server('HTTP_HOST');
|
|
if ($host === '') {
|
|
$host = Http::server('SERVER_NAME', 'localhost');
|
|
}
|
|
$host = preg_replace('/:\d+$/', '', $host) ?? $host;
|
|
|
|
return $scheme . '://' . $host . $path;
|
|
}
|
|
|
|
/**
|
|
* @param array<string,string|int> $auth
|
|
*/
|
|
private function writeLoginTicket(array $auth, string $database): string
|
|
{
|
|
$ticketDir = rtrim($this->settings->adminerSsoDir(), '/') . '/tickets';
|
|
if (!is_dir($ticketDir) && !@mkdir($ticketDir, 0755, true) && !is_dir($ticketDir)) {
|
|
throw new RuntimeException('Nie można utworzyć katalogu ticketów phpMyAdmin: ' . $ticketDir);
|
|
}
|
|
|
|
$this->cleanupExpiredTickets($ticketDir);
|
|
|
|
$token = bin2hex(random_bytes(32));
|
|
$payload = [
|
|
'version' => 1,
|
|
'created_at' => time(),
|
|
'expires_at' => time() + $this->settings->adminerTicketTtl(),
|
|
'auth' => $auth,
|
|
'route' => '/database/structure',
|
|
'db' => $database,
|
|
];
|
|
$encoded = json_encode($payload, JSON_UNESCAPED_SLASHES);
|
|
if (!is_string($encoded) || $encoded === '') {
|
|
throw new RuntimeException('Nie udało się zakodować ticketu phpMyAdmin.');
|
|
}
|
|
|
|
$path = $ticketDir . '/' . $token . '.json';
|
|
$tmpPath = $path . '.tmp.' . bin2hex(random_bytes(4));
|
|
if (@file_put_contents($tmpPath, $encoded, LOCK_EX) === false) {
|
|
throw new RuntimeException('Nie udało się zapisać ticketu phpMyAdmin.');
|
|
}
|
|
@chmod($tmpPath, 0644);
|
|
if (!@rename($tmpPath, $path)) {
|
|
@unlink($tmpPath);
|
|
throw new RuntimeException('Nie udało się aktywować ticketu phpMyAdmin.');
|
|
}
|
|
|
|
return $token;
|
|
}
|
|
|
|
private function cleanupExpiredTickets(string $ticketDir): void
|
|
{
|
|
foreach (glob($ticketDir . '/*.json') ?: [] as $path) {
|
|
if (!is_file($path)) {
|
|
continue;
|
|
}
|
|
$raw = @file_get_contents($path);
|
|
$decoded = is_string($raw) ? json_decode($raw, true) : null;
|
|
$expiresAt = is_array($decoded) ? (int)($decoded['expires_at'] ?? 0) : 0;
|
|
if ($expiresAt > 0 && $expiresAt >= time()) {
|
|
continue;
|
|
}
|
|
@unlink($path);
|
|
}
|
|
}
|
|
|
|
private function generateTemporaryRoleName(): string
|
|
{
|
|
$base = strtolower($this->daUser->username());
|
|
$base = preg_replace('/[^a-z0-9_]+/', '_', $base) ?? 'user';
|
|
$base = trim($base, '_');
|
|
if ($base === '') {
|
|
$base = 'user';
|
|
}
|
|
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$suffix = bin2hex(random_bytes(6));
|
|
$maxBaseLen = NamePolicy::MAX_IDENTIFIER_LEN - strlen('da_tmp_phpmyadmin_') - 1 - strlen($suffix);
|
|
if ($maxBaseLen < 1) {
|
|
$maxBaseLen = 1;
|
|
}
|
|
$safeBase = substr($base, 0, $maxBaseLen);
|
|
$role = 'da_tmp_phpmyadmin_' . $safeBase . '_' . $suffix;
|
|
if (!$this->db->roleExists($role)) {
|
|
return $role;
|
|
}
|
|
}
|
|
|
|
throw new RuntimeException('Nie udało się wygenerować unikalnego tymczasowego użytkownika dla phpMyAdmin.');
|
|
}
|
|
|
|
private function generateSecret(int $len): string
|
|
{
|
|
$value = rtrim(strtr(base64_encode(random_bytes(48)), '+/', 'AZ'), '=');
|
|
if (strlen($value) >= $len) {
|
|
return substr($value, 0, $len);
|
|
}
|
|
return str_pad($value, $len, 'x');
|
|
}
|
|
}
|