Files
alt-mysql/docs/superpowers/specs/2026-07-04-phpmyadmin-all-databases-design.md
Marek Miklewicz 722ce5beaa Add design spec for phpMyAdmin all-databases top-nav access
Documents the approved design for restoring "grant access to every
owned database" behavior specifically for the top-nav PHPMYADMIN
button, while keeping the per-row "Zaloguj do bazy" button scoped to
one database.
2026-07-04 21:16:06 +02:00

5.5 KiB

phpMyAdmin: all-databases access from the top-nav button

Context

The plugin's page header (AppContext::renderTopActions()) renders a top-level "PHPMYADMIN" button, separate from the per-row "Zaloguj do bazy" button in the database table. Both POST to the same open_phpmyadmin.raw action and go through PhpMyAdminSso, but they differ in one respect: the row button always sends adminer_db=<name>; the top-nav button never sends adminer_db at all.

A recent security fix (scoping the temporary SSO MySQL role to only the requested database instead of every database the account owns) treated both cases identically: when no database was requested, it defaulted to the first owned database and granted only that one. This is correct for the row button, but broke the top-nav button's intent, which is to open phpMyAdmin with access to every database the DirectAdmin user owns.

Goal

Restore "access to all owned databases" for the top-nav PHPMYADMIN button, without reintroducing over-broad access for the per-row button.

Design

PhpMyAdminSso::issueLoginPayload()

Treat "no database requested" ($requestedDatabase === null or empty after trim) as its own explicit case, not as "default to the first database and scope to it."

Extract a pure, testable decision function:

/**
 * @param string[] $ownedDatabases
 * @return string[]
 */
private static function determineGrantTargets(?string $requestedDatabase, array $ownedDatabases): array
{
    if ($requestedDatabase === null || $requestedDatabase === '') {
        return $ownedDatabases;
    }
    return in_array($requestedDatabase, $ownedDatabases, true) ? [$requestedDatabase] : [];
}

This replaces the current determineGrantTargets(string $targetDatabase, array $ownedDatabases), which took the already-resolved target database and could no longer distinguish "explicitly requested this one" from "nothing was requested."

issueLoginPayload() keeps resolving a concrete $targetDatabase for the phpMyAdmin landing page route (route=/database/structure&db=...) exactly as it does today — defaulting to the first owned database when none was requested. That landing behavior is unaffected; only the grant scope and the only_db restriction change.

A new local value, $restrictToDatabase, is derived alongside the grant targets: empty string when granting all databases, or the single target database name when scoped to one. This value is written into the ticket payload as a new field, restrict_db, separate from the existing db field (which continues to mean "which database to land on," not "which database(s) to allow").

Ticket payload

{
  "version": 1,
  "created_at": 0,
  "expires_at": 0,
  "auth": { "user": "...", "password": "...", "host": "...", "port": 0, "db": "..." },
  "route": "/database/structure",
  "db": "landing_database",
  "restrict_db": ""
}

restrict_db is "" for the top-nav (all-databases) case, or the specific database name for the per-row case.

da_login.php (generated by phpmyadmin_install.sh)

When consuming a ticket, store restrict_db into the SSO session under a new key, alongside the existing auth payload:

$_SESSION['DA_MYSQL_PHPMYADMIN_AUTH'] = $ticket['auth'];
$_SESSION['DA_MYSQL_PHPMYADMIN_RESTRICT_DB'] = (string)($ticket['restrict_db'] ?? '');

config.inc.php's only_db builder

The existing da_mysql_phpmyadmin_only_db() function currently reads $_SESSION['DA_MYSQL_PHPMYADMIN_AUTH']['db']. It will instead read the new DA_MYSQL_PHPMYADMIN_RESTRICT_DB session key, defaulting to '' (no restriction) when absent. An empty value means phpMyAdmin's only_db stays unset, showing every database the temporary role has been granted.

Data flow

  • Row button: POST includes adminer_db=admin_wiesio → handler passes 'admin_wiesio' to issueLoginPayload() → grants only admin_wiesio, restrict_db=admin_wiesio → phpMyAdmin shows only that database.
  • Top-nav button: POST has no adminer_db field → handler's existing if ($requestedDb === '') { $requestedDb = null; } passes nullissueLoginPayload(null) → grants every owned database, restrict_db='' → phpMyAdmin shows all databases, landing on the first one's structure page (unchanged from today).

Error handling

Unchanged. If the DirectAdmin user owns zero databases, both entry points still throw "Użytkownik nie ma żadnej bazy MySQL do otwarcia w phpMyAdmin." before reaching the grant-scope logic.

Naming

No new "Adminer"-branded identifiers. New field/session key names (restrict_db, DA_MYSQL_PHPMYADMIN_RESTRICT_DB) follow the existing da_mysql_* / DA_MYSQL_* convention already used throughout the generated phpMyAdmin integration files. Pre-existing legacy names like MySQLService::grantTemporaryAdminerAccess() are out of scope for this change.

Testing

  • tests/phpmyadmin_sso_test.php: update the existing determineGrantTargets assertions for the new ?string signature, and add a case asserting that null/'' returns every owned database.
  • tests/phpmyadmin_install_test.sh: extend the existing only_db PHP-fixture check with a second scenario — a session with DA_MYSQL_PHPMYADMIN_RESTRICT_DB unset/empty — asserting $cfg['Servers'][1]['only_db'] is '' (not the landing db value).

Out of scope

  • Renaming legacy "Adminer"-named methods/settings elsewhere in the codebase.
  • Any change to the row button's behavior (already correct).
  • Any new UI element — the top-nav button already exists and already omits adminer_db; no markup changes are needed.