Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ef4fd79ce | |||
| 766380c9eb | |||
| 9a5781e5ed | |||
| 95b6d774fc | |||
| 722ce5beaa |
@@ -0,0 +1,423 @@
|
|||||||
|
# phpMyAdmin All-Databases Top-Nav Access Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Restore "grant access to every owned database" behavior for the top-nav PHPMYADMIN button while keeping the per-row "Zaloguj do bazy" button scoped to a single database.
|
||||||
|
|
||||||
|
**Architecture:** `PhpMyAdminSso::issueLoginPayload()` treats "no database requested" as its own explicit case (grant all, don't restrict) instead of defaulting it into "scope to the first database." A new `restrict_db` field flows from the ticket payload into the phpMyAdmin SSO session, and `config.inc.php`'s `only_db` builder reads that field instead of the landing-page `db` field.
|
||||||
|
|
||||||
|
**Tech Stack:** PHP 8 (plugin classes), bash heredocs generating standalone PHP files (`da_login.php`, `config.inc.php`) consumed by phpMyAdmin.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- No new "Adminer"-branded identifiers; new names use `phpmyadmin`/`PhpMyAdmin`/`DA_MYSQL_PHPMYADMIN` conventions (per spec `docs/superpowers/specs/2026-07-04-phpmyadmin-all-databases-design.md`).
|
||||||
|
- The per-row "Zaloguj do bazy" button's behavior (grant + restrict to one database) must not change.
|
||||||
|
- Every task must leave `bash tests/*.sh` and `php tests/*.php` green.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Scope-aware grant targets and ticket payload in PhpMyAdminSso
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `exec/lib/PhpMyAdminSso.php:20-96` (`issueLoginPayload`, `determineGrantTargets`, `writeLoginTicket`)
|
||||||
|
- Test: `tests/phpmyadmin_sso_test.php`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: nothing new.
|
||||||
|
- Produces: `PhpMyAdminSso::determineGrantTargets(?string $requestedDatabase, array $ownedDatabases): array` (signature change — was `string $targetDatabase`), and a new `restrict_db` string key in the ticket JSON payload written by `writeLoginTicket()`. Task 2 reads this exact key name from the ticket.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Open `tests/phpmyadmin_sso_test.php`. Replace the existing `determineGrantTargets` block (currently lines 64-80) with:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// --- 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'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Then replace the `writeLoginTicket` invocation block (currently lines 96-118) with:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$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)');
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `cd /Users/marek/GPT/ChatGPT/da_plugins/alt-mysql/src && php tests/phpmyadmin_sso_test.php`
|
||||||
|
Expected: FAIL — either an arity/type error calling `writeLoginTicket` with 4 arguments (current signature only accepts 3), or the `null`/`''` `determineGrantTargets` assertions failing (current implementation treats a `string $targetDatabase` positionally, `null`/`''` won't behave as "grant all" yet).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
In `exec/lib/PhpMyAdminSso.php`, replace the block from `$targetDatabase = '';` through `$targetDatabase = $databaseNames[0]; }` (current lines 42-54) with:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$normalizedRequestedDatabase = null;
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
$normalizedRequestedDatabase = $requestedDatabase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetDatabase = $normalizedRequestedDatabase ?? $databaseNames[0];
|
||||||
|
$restrictToDatabase = $normalizedRequestedDatabase ?? '';
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the grant loop (current lines 61-72):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$this->db->createTemporaryRole($role, $password, $expiresAt);
|
||||||
|
try {
|
||||||
|
foreach (self::determineGrantTargets($normalizedRequestedDatabase, $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);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the `writeLoginTicket` call (current line 81):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$ticket = $this->writeLoginTicket($auth, $targetDatabase, $restrictToDatabase);
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `determineGrantTargets` (current lines 89-96):
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* @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] : [];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `writeLoginTicket`'s signature and payload (current lines 139-156):
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* @param array<string,string|int> $auth
|
||||||
|
*/
|
||||||
|
private function writeLoginTicket(array $auth, string $database, string $restrictToDatabase): 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,
|
||||||
|
'restrict_db' => $restrictToDatabase,
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `cd /Users/marek/GPT/ChatGPT/da_plugins/alt-mysql/src && php tests/phpmyadmin_sso_test.php`
|
||||||
|
Expected: `phpmyadmin_sso_test: OK`
|
||||||
|
|
||||||
|
Also run the full local suite to confirm no regressions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/marek/GPT/ChatGPT/da_plugins/alt-mysql/src
|
||||||
|
find . -name "*.php" | xargs -I{} php -l {} 2>&1 | grep -v "No syntax errors"
|
||||||
|
cd tests
|
||||||
|
for f in *_test.sh; do bash "$f" || echo "FAIL: $f"; done
|
||||||
|
for f in *_test.php; do php "$f" || echo "FAIL: $f"; done
|
||||||
|
```
|
||||||
|
Expected: every line ends `OK`, no `FAIL:` lines, no `php -l` output.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/marek/GPT/ChatGPT/da_plugins/alt-mysql/src
|
||||||
|
git add exec/lib/PhpMyAdminSso.php tests/phpmyadmin_sso_test.php
|
||||||
|
git commit -m "Grant all owned databases when phpMyAdmin SSO has no specific target
|
||||||
|
|
||||||
|
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."
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Thread restrict_db through da_login.php session and config.inc.php only_db
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/setup/phpmyadmin_install.sh` (the `da_login.php` heredoc around line 517, and the `config.inc.php` heredoc's `da_mysql_phpmyadmin_only_db()` function around lines 340-361)
|
||||||
|
- Test: `tests/phpmyadmin_install_test.sh`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: ticket JSON's `restrict_db` key produced by Task 1's `writeLoginTicket()`.
|
||||||
|
- Produces: PHP session key `$_SESSION['DA_MYSQL_PHPMYADMIN_RESTRICT_DB']`, read by `da_mysql_phpmyadmin_only_db()`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Open `tests/phpmyadmin_install_test.sh`. Find the existing `only_db` scenario (the block starting with the comment `# config.inc.php must restrict phpMyAdmin's visible database...` and the `ONLY_DB=` assignment). Immediately after that block's `fail` line, add a second scenario:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# When no specific database was requested (the top-nav PHPMYADMIN button),
|
||||||
|
# only_db must be empty so phpMyAdmin shows every granted database.
|
||||||
|
ALL_DB_ONLY_DB="$(php -d session.save_path="$SESSION_DIR" -r '
|
||||||
|
session_name("DA_MYSQL_PHPMYADMIN");
|
||||||
|
session_id("alldbtest");
|
||||||
|
session_start();
|
||||||
|
$_SESSION["DA_MYSQL_PHPMYADMIN_AUTH"] = ["user" => "x", "password" => "y", "db" => "demo_landing_db"];
|
||||||
|
$_SESSION["DA_MYSQL_PHPMYADMIN_RESTRICT_DB"] = "";
|
||||||
|
session_write_close();
|
||||||
|
|
||||||
|
$_COOKIE["DA_MYSQL_PHPMYADMIN"] = "alldbtest";
|
||||||
|
$cfg = [];
|
||||||
|
include $argv[1];
|
||||||
|
echo $cfg["Servers"][1]["only_db"] ?? "(unset)";
|
||||||
|
' "$CONFIG_INC")"
|
||||||
|
[ "$ALL_DB_ONLY_DB" = "" ] \
|
||||||
|
|| fail "config.inc.php only_db must be empty for an all-databases login, got: '$ALL_DB_ONLY_DB'"
|
||||||
|
```
|
||||||
|
|
||||||
|
Also update the *existing* `only_db` scenario's session payload to set `DA_MYSQL_PHPMYADMIN_RESTRICT_DB` explicitly (matching what Task 1's ticket now produces) instead of relying on the auth `db` field:
|
||||||
|
|
||||||
|
Find:
|
||||||
|
```bash
|
||||||
|
$_SESSION["DA_MYSQL_PHPMYADMIN_AUTH"] = ["user" => "x", "password" => "y", "db" => "demo_target_db"];
|
||||||
|
session_write_close();
|
||||||
|
|
||||||
|
$_COOKIE["DA_MYSQL_PHPMYADMIN"] = "onlydbtest";
|
||||||
|
```
|
||||||
|
Replace with:
|
||||||
|
```bash
|
||||||
|
$_SESSION["DA_MYSQL_PHPMYADMIN_AUTH"] = ["user" => "x", "password" => "y", "db" => "demo_target_db"];
|
||||||
|
$_SESSION["DA_MYSQL_PHPMYADMIN_RESTRICT_DB"] = "demo_target_db";
|
||||||
|
session_write_close();
|
||||||
|
|
||||||
|
$_COOKIE["DA_MYSQL_PHPMYADMIN"] = "onlydbtest";
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `cd /Users/marek/GPT/ChatGPT/da_plugins/alt-mysql/src && bash tests/phpmyadmin_install_test.sh`
|
||||||
|
Expected: FAIL on `config.inc.php only_db should be scoped to the SSO ticket's database` (since `only_db` still reads the auth `db` field, which is `demo_target_db` regardless of the new `RESTRICT_DB` session key — this proves the test exercises the not-yet-updated code).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
In `scripts/setup/phpmyadmin_install.sh`, replace `da_mysql_phpmyadmin_only_db()`'s body (current lines 340-361):
|
||||||
|
|
||||||
|
```php
|
||||||
|
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();
|
||||||
|
$db = (string)($_SESSION['DA_MYSQL_PHPMYADMIN_RESTRICT_DB'] ?? '');
|
||||||
|
session_write_close();
|
||||||
|
|
||||||
|
return $db;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then find the `da_login.php` heredoc's session-writing line (current line 517):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$_SESSION['DA_MYSQL_PHPMYADMIN_AUTH'] = $ticket['auth'];
|
||||||
|
session_write_close();
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace with:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$_SESSION['DA_MYSQL_PHPMYADMIN_AUTH'] = $ticket['auth'];
|
||||||
|
$_SESSION['DA_MYSQL_PHPMYADMIN_RESTRICT_DB'] = (string)($ticket['restrict_db'] ?? '');
|
||||||
|
session_write_close();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `cd /Users/marek/GPT/ChatGPT/da_plugins/alt-mysql/src && bash tests/phpmyadmin_install_test.sh`
|
||||||
|
Expected: `phpmyadmin_install_test: OK`
|
||||||
|
|
||||||
|
Run the full local suite again:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/marek/GPT/ChatGPT/da_plugins/alt-mysql/src
|
||||||
|
bash -n scripts/setup/phpmyadmin_install.sh
|
||||||
|
cd tests
|
||||||
|
for f in *_test.sh; do bash "$f" || echo "FAIL: $f"; done
|
||||||
|
for f in *_test.php; do php "$f" || echo "FAIL: $f"; done
|
||||||
|
```
|
||||||
|
Expected: every line ends `OK`, no `FAIL:` lines.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/marek/GPT/ChatGPT/da_plugins/alt-mysql/src
|
||||||
|
git add scripts/setup/phpmyadmin_install.sh tests/phpmyadmin_install_test.sh
|
||||||
|
git commit -m "Read only_db restriction from a dedicated session key
|
||||||
|
|
||||||
|
da_login.php now stores the ticket's restrict_db into
|
||||||
|
DA_MYSQL_PHPMYADMIN_RESTRICT_DB, and config.inc.php's only_db builder
|
||||||
|
reads that key instead of the landing-page db field, so an
|
||||||
|
all-databases login (empty restrict_db) leaves phpMyAdmin unrestricted."
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Version bump, full verification, commit, push, package
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `plugin.conf`
|
||||||
|
- Modify: `tests/plugin_identity_test.sh`
|
||||||
|
- Modify: `tests/package_archive_test.sh`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: nothing new.
|
||||||
|
- Produces: `archives/<version>/alt-mysql.tar.gz`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Bump the version**
|
||||||
|
|
||||||
|
Read `plugin.conf`, note the current `version=X.Y.Z` line, and bump the patch number by 1 (e.g. `1.2.14` → `1.2.15`). Apply the same new version string to:
|
||||||
|
- `plugin.conf`'s `version=` line
|
||||||
|
- `tests/plugin_identity_test.sh`'s `grep -Fxq "version=X.Y.Z"` line
|
||||||
|
- `tests/package_archive_test.sh`'s `archives/X.Y.Z/alt-mysql.tar.gz` default path and its `version=X.Y.Z` grep line
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the full test suite**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/marek/GPT/ChatGPT/da_plugins/alt-mysql/src
|
||||||
|
find . -name "*.php" | xargs -I{} php -l {} 2>&1 | grep -v "No syntax errors"
|
||||||
|
find . -name "*.sh" | xargs -I{} bash -n {} 2>&1
|
||||||
|
cd tests
|
||||||
|
for f in *_test.sh; do bash "$f" || echo "FAIL: $f"; done
|
||||||
|
for f in *_test.php; do php "$f" || echo "FAIL: $f"; done
|
||||||
|
```
|
||||||
|
Expected: every test prints `OK`; `package_archive_test.sh` is expected to fail here (archive not built yet) — that single failure is acceptable at this step.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit the version bump**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/marek/GPT/ChatGPT/da_plugins/alt-mysql/src
|
||||||
|
git add plugin.conf tests/plugin_identity_test.sh tests/package_archive_test.sh
|
||||||
|
git commit -m "Bump version to <new-version>"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Push to Gitea**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/marek/GPT/ChatGPT/da_plugins/alt-mysql/src && git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Build the release package**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/marek/GPT/ChatGPT/da_plugins/alt-mysql
|
||||||
|
mkdir -p archives/<new-version>
|
||||||
|
STAGE="$(mktemp -d)"
|
||||||
|
cp -a src/. "$STAGE"/
|
||||||
|
rm -rf "$STAGE/tests" "$STAGE/.git"
|
||||||
|
rm -f "$STAGE/.gitignore" "$STAGE/error.log"
|
||||||
|
find "$STAGE" -name ".DS_Store" -delete
|
||||||
|
find "$STAGE" -name "._*" -delete
|
||||||
|
tar -C "$STAGE" -czf archives/<new-version>/alt-mysql.tar.gz .
|
||||||
|
rm -rf "$STAGE"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Verify the package**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/marek/GPT/ChatGPT/da_plugins/alt-mysql/src && bash tests/package_archive_test.sh
|
||||||
|
```
|
||||||
|
Expected: `package_archive_test: OK`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Notes
|
||||||
|
|
||||||
|
- **Spec coverage:** Task 1 covers the spec's `determineGrantTargets`/ticket-payload section. Task 2 covers the `da_login.php` session and `config.inc.php` `only_db` sections. Task 3 covers packaging (spec's "out of scope" items — Adminer renaming, UI changes — are correctly not tasked).
|
||||||
|
- **Placeholder scan:** no TBD/TODO; all code blocks are complete and copy-pasteable.
|
||||||
|
- **Type consistency:** `determineGrantTargets(?string $requestedDatabase, array $ownedDatabases): array` and `writeLoginTicket(array $auth, string $database, string $restrictToDatabase): string` signatures match between Task 1's implementation step and its test step. The `restrict_db` key name matches between Task 1's ticket payload and Task 2's session-read code.
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# 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:
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$_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 `null` →
|
||||||
|
`issueLoginPayload(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.
|
||||||
+14
-10
@@ -39,19 +39,19 @@ final class PhpMyAdminSso
|
|||||||
}
|
}
|
||||||
$databaseNames = array_values(array_unique($databaseNames));
|
$databaseNames = array_values(array_unique($databaseNames));
|
||||||
|
|
||||||
$targetDatabase = '';
|
$normalizedRequestedDatabase = null;
|
||||||
if ($requestedDatabase !== null) {
|
if ($requestedDatabase !== null) {
|
||||||
$requestedDatabase = trim($requestedDatabase);
|
$requestedDatabase = trim($requestedDatabase);
|
||||||
if ($requestedDatabase !== '') {
|
if ($requestedDatabase !== '') {
|
||||||
if (!in_array($requestedDatabase, $databaseNames, true)) {
|
if (!in_array($requestedDatabase, $databaseNames, true)) {
|
||||||
throw new RuntimeException('Wskazana baza danych nie należy do zalogowanego użytkownika.');
|
throw new RuntimeException('Wskazana baza danych nie należy do zalogowanego użytkownika.');
|
||||||
}
|
}
|
||||||
$targetDatabase = $requestedDatabase;
|
$normalizedRequestedDatabase = $requestedDatabase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($targetDatabase === '') {
|
|
||||||
$targetDatabase = $databaseNames[0];
|
$targetDatabase = $normalizedRequestedDatabase ?? $databaseNames[0];
|
||||||
}
|
$restrictToDatabase = $normalizedRequestedDatabase ?? '';
|
||||||
|
|
||||||
$roleTtl = max($this->settings->adminerRoleTtl(), $this->settings->adminerSessionTtl() + 120);
|
$roleTtl = max($this->settings->adminerRoleTtl(), $this->settings->adminerSessionTtl() + 120);
|
||||||
$role = $this->generateTemporaryRoleName();
|
$role = $this->generateTemporaryRoleName();
|
||||||
@@ -60,7 +60,7 @@ final class PhpMyAdminSso
|
|||||||
|
|
||||||
$this->db->createTemporaryRole($role, $password, $expiresAt);
|
$this->db->createTemporaryRole($role, $password, $expiresAt);
|
||||||
try {
|
try {
|
||||||
foreach (self::determineGrantTargets($targetDatabase, $databaseNames) as $dbName) {
|
foreach (self::determineGrantTargets($normalizedRequestedDatabase, $databaseNames) as $dbName) {
|
||||||
$this->db->grantTemporaryAdminerAccess($dbName, $role);
|
$this->db->grantTemporaryAdminerAccess($dbName, $role);
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
@@ -78,7 +78,7 @@ final class PhpMyAdminSso
|
|||||||
'port' => $this->db->connectionEndpoint()['port'],
|
'port' => $this->db->connectionEndpoint()['port'],
|
||||||
'db' => $targetDatabase,
|
'db' => $targetDatabase,
|
||||||
];
|
];
|
||||||
$ticket = $this->writeLoginTicket($auth, $targetDatabase);
|
$ticket = $this->writeLoginTicket($auth, $targetDatabase, $restrictToDatabase);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'target' => $this->buildPhpMyAdminLoginUrl($targetDatabase, $ticket),
|
'target' => $this->buildPhpMyAdminLoginUrl($targetDatabase, $ticket),
|
||||||
@@ -90,9 +90,12 @@ final class PhpMyAdminSso
|
|||||||
* @param string[] $ownedDatabases
|
* @param string[] $ownedDatabases
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
private static function determineGrantTargets(string $targetDatabase, array $ownedDatabases): array
|
private static function determineGrantTargets(?string $requestedDatabase, array $ownedDatabases): array
|
||||||
{
|
{
|
||||||
return in_array($targetDatabase, $ownedDatabases, true) ? [$targetDatabase] : [];
|
if ($requestedDatabase === null || $requestedDatabase === '') {
|
||||||
|
return $ownedDatabases;
|
||||||
|
}
|
||||||
|
return in_array($requestedDatabase, $ownedDatabases, true) ? [$requestedDatabase] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildPhpMyAdminLoginUrl(string $database, string $ticket): string
|
private function buildPhpMyAdminLoginUrl(string $database, string $ticket): string
|
||||||
@@ -136,7 +139,7 @@ final class PhpMyAdminSso
|
|||||||
/**
|
/**
|
||||||
* @param array<string,string|int> $auth
|
* @param array<string,string|int> $auth
|
||||||
*/
|
*/
|
||||||
private function writeLoginTicket(array $auth, string $database): string
|
private function writeLoginTicket(array $auth, string $database, string $restrictToDatabase): string
|
||||||
{
|
{
|
||||||
$ticketDir = rtrim($this->settings->adminerSsoDir(), '/') . '/tickets';
|
$ticketDir = rtrim($this->settings->adminerSsoDir(), '/') . '/tickets';
|
||||||
if (!is_dir($ticketDir) && !@mkdir($ticketDir, 0755, true) && !is_dir($ticketDir)) {
|
if (!is_dir($ticketDir) && !@mkdir($ticketDir, 0755, true) && !is_dir($ticketDir)) {
|
||||||
@@ -153,6 +156,7 @@ final class PhpMyAdminSso
|
|||||||
'auth' => $auth,
|
'auth' => $auth,
|
||||||
'route' => '/database/structure',
|
'route' => '/database/structure',
|
||||||
'db' => $database,
|
'db' => $database,
|
||||||
|
'restrict_db' => $restrictToDatabase,
|
||||||
];
|
];
|
||||||
$encoded = json_encode($payload, JSON_UNESCAPED_SLASHES);
|
$encoded = json_encode($payload, JSON_UNESCAPED_SLASHES);
|
||||||
if (!is_string($encoded) || $encoded === '') {
|
if (!is_string($encoded) || $encoded === '') {
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ name=alt-mysql
|
|||||||
id=alt-mysql
|
id=alt-mysql
|
||||||
type=user
|
type=user
|
||||||
author=HITME.PL
|
author=HITME.PL
|
||||||
version=1.2.14
|
version=1.2.15
|
||||||
active=no
|
active=no
|
||||||
installed=no
|
installed=no
|
||||||
user_run_as=root
|
user_run_as=root
|
||||||
|
|||||||
@@ -353,8 +353,7 @@ function da_mysql_phpmyadmin_only_db(): string
|
|||||||
session_name($sessionName);
|
session_name($sessionName);
|
||||||
session_id($sessionId);
|
session_id($sessionId);
|
||||||
session_start();
|
session_start();
|
||||||
$auth = $_SESSION['DA_MYSQL_PHPMYADMIN_AUTH'] ?? [];
|
$db = (string)($_SESSION['DA_MYSQL_PHPMYADMIN_RESTRICT_DB'] ?? '');
|
||||||
$db = is_array($auth) ? (string)($auth['db'] ?? '') : '';
|
|
||||||
session_write_close();
|
session_write_close();
|
||||||
|
|
||||||
return $db;
|
return $db;
|
||||||
@@ -515,6 +514,7 @@ session_set_cookie_params([
|
|||||||
]);
|
]);
|
||||||
session_start();
|
session_start();
|
||||||
$_SESSION['DA_MYSQL_PHPMYADMIN_AUTH'] = $ticket['auth'];
|
$_SESSION['DA_MYSQL_PHPMYADMIN_AUTH'] = $ticket['auth'];
|
||||||
|
$_SESSION['DA_MYSQL_PHPMYADMIN_RESTRICT_DB'] = (string)($ticket['restrict_db'] ?? '');
|
||||||
session_write_close();
|
session_write_close();
|
||||||
|
|
||||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
ARCHIVE="${1:-$(cd "$PLUGIN_DIR/.." && pwd)/archives/1.2.14/alt-mysql.tar.gz}"
|
ARCHIVE="${1:-$(cd "$PLUGIN_DIR/.." && pwd)/archives/1.2.15/alt-mysql.tar.gz}"
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
echo "FAIL: $*" >&2
|
echo "FAIL: $*" >&2
|
||||||
@@ -37,7 +37,7 @@ fi
|
|||||||
CONF="$(tar -xOzf "$ARCHIVE" ./plugin.conf 2>/dev/null || tar -xOzf "$ARCHIVE" plugin.conf)"
|
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 "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 "id=alt-mysql" || fail "archive plugin id is not alt-mysql"
|
||||||
printf '%s\n' "$CONF" | grep -Fxq "version=1.2.14" || fail "archive plugin version is not 1.2.14"
|
printf '%s\n' "$CONF" | grep -Fxq "version=1.2.15" || fail "archive plugin version is not 1.2.15"
|
||||||
|
|
||||||
SETTINGS="$(tar -xOzf "$ARCHIVE" ./plugin-settings.conf 2>/dev/null || tar -xOzf "$ARCHIVE" plugin-settings.conf)"
|
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" \
|
printf '%s\n' "$SETTINGS" | grep -Fxq "PHPMYADMIN_INSTALL_DIR=/var/www/html/alt-mysql-phpmyadmin" \
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ ONLY_DB="$(php -d session.save_path="$SESSION_DIR" -r '
|
|||||||
session_id("onlydbtest");
|
session_id("onlydbtest");
|
||||||
session_start();
|
session_start();
|
||||||
$_SESSION["DA_MYSQL_PHPMYADMIN_AUTH"] = ["user" => "x", "password" => "y", "db" => "demo_target_db"];
|
$_SESSION["DA_MYSQL_PHPMYADMIN_AUTH"] = ["user" => "x", "password" => "y", "db" => "demo_target_db"];
|
||||||
|
$_SESSION["DA_MYSQL_PHPMYADMIN_RESTRICT_DB"] = "demo_target_db";
|
||||||
session_write_close();
|
session_write_close();
|
||||||
|
|
||||||
$_COOKIE["DA_MYSQL_PHPMYADMIN"] = "onlydbtest";
|
$_COOKIE["DA_MYSQL_PHPMYADMIN"] = "onlydbtest";
|
||||||
@@ -140,6 +141,24 @@ ONLY_DB="$(php -d session.save_path="$SESSION_DIR" -r '
|
|||||||
[ "$ONLY_DB" = "demo_target_db" ] \
|
[ "$ONLY_DB" = "demo_target_db" ] \
|
||||||
|| fail "config.inc.php only_db should be scoped to the SSO ticket's database, got: '$ONLY_DB'"
|
|| fail "config.inc.php only_db should be scoped to the SSO ticket's database, got: '$ONLY_DB'"
|
||||||
|
|
||||||
|
# When no specific database was requested (the top-nav PHPMYADMIN button),
|
||||||
|
# only_db must be empty so phpMyAdmin shows every granted database.
|
||||||
|
ALL_DB_ONLY_DB="$(php -d session.save_path="$SESSION_DIR" -r '
|
||||||
|
session_name("DA_MYSQL_PHPMYADMIN");
|
||||||
|
session_id("alldbtest");
|
||||||
|
session_start();
|
||||||
|
$_SESSION["DA_MYSQL_PHPMYADMIN_AUTH"] = ["user" => "x", "password" => "y", "db" => "demo_landing_db"];
|
||||||
|
$_SESSION["DA_MYSQL_PHPMYADMIN_RESTRICT_DB"] = "";
|
||||||
|
session_write_close();
|
||||||
|
|
||||||
|
$_COOKIE["DA_MYSQL_PHPMYADMIN"] = "alldbtest";
|
||||||
|
$cfg = [];
|
||||||
|
include $argv[1];
|
||||||
|
echo $cfg["Servers"][1]["only_db"] ?? "(unset)";
|
||||||
|
' "$CONFIG_INC")"
|
||||||
|
[ "$ALL_DB_ONLY_DB" = "" ] \
|
||||||
|
|| fail "config.inc.php only_db must be empty for an all-databases login, got: '$ALL_DB_ONLY_DB'"
|
||||||
|
|
||||||
grep -Fq 'return [$username, $password];' "$SIGNON_SCRIPT" \
|
grep -Fq 'return [$username, $password];' "$SIGNON_SCRIPT" \
|
||||||
|| fail "signon script should return username/password only"
|
|| 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" \
|
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" \
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ assert_true($mysqlServiceClass->hasConstant('TMP_ROLE_HOST'), 'MySQLService must
|
|||||||
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');
|
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 ---
|
// --- 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);
|
$ssoClassForGrants = new ReflectionClass(PhpMyAdminSso::class);
|
||||||
assert_true($ssoClassForGrants->hasMethod('determineGrantTargets'), 'PhpMyAdminSso must expose a determineGrantTargets() decision method');
|
assert_true($ssoClassForGrants->hasMethod('determineGrantTargets'), 'PhpMyAdminSso must expose a determineGrantTargets() decision method');
|
||||||
@@ -71,12 +72,22 @@ $determineGrantTargets->setAccessible(true);
|
|||||||
assert_same(
|
assert_same(
|
||||||
['demo_target'],
|
['demo_target'],
|
||||||
$determineGrantTargets->invoke(null, 'demo_target', ['demo_target', 'demo_other']),
|
$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'
|
'requesting one database must only grant that database, not every database the user owns'
|
||||||
);
|
);
|
||||||
assert_same(
|
assert_same(
|
||||||
[],
|
[],
|
||||||
$determineGrantTargets->invoke(null, 'not_owned', ['demo_target', 'demo_other']),
|
$determineGrantTargets->invoke(null, 'not_owned', ['demo_target', 'demo_other']),
|
||||||
'a target database that is not in the owned list must never be granted'
|
'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 ---
|
// --- phpMyAdmin login ticket files must not be world-readable ---
|
||||||
@@ -102,7 +113,7 @@ $auth = [
|
|||||||
'port' => 33033,
|
'port' => 33033,
|
||||||
'db' => 'demo_db',
|
'db' => 'demo_db',
|
||||||
];
|
];
|
||||||
$token = $writeTicket->invoke($sso, $auth, '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');
|
assert_true(preg_match('/\A[a-f0-9]{64}\z/', $token) === 1, 'ticket token must be a 64 char lowercase hex string');
|
||||||
|
|
||||||
@@ -114,9 +125,15 @@ assert_same(0640, $mode, 'ticket files contain a plaintext MySQL password and mu
|
|||||||
|
|
||||||
$decoded = json_decode((string)file_get_contents($ticketPath), true);
|
$decoded = json_decode((string)file_get_contents($ticketPath), true);
|
||||||
assert_true(is_array($decoded), 'ticket file must contain valid JSON');
|
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('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');
|
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
|
// cleanup
|
||||||
array_map('unlink', glob($ssoDir . '/tickets/*') ?: []);
|
array_map('unlink', glob($ssoDir . '/tickets/*') ?: []);
|
||||||
@rmdir($ssoDir . '/tickets');
|
@rmdir($ssoDir . '/tickets');
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ case "$(basename "$PLUGIN_DIR")" in
|
|||||||
esac
|
esac
|
||||||
grep -Fxq "name=alt-mysql" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf name is not alt-mysql"
|
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 "id=alt-mysql" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf id is not alt-mysql"
|
||||||
grep -Fxq "version=1.2.14" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf version is not 1.2.14"
|
grep -Fxq "version=1.2.15" "$PLUGIN_DIR/plugin.conf" || fail "plugin.conf version is not 1.2.15"
|
||||||
grep -Fq "return '/CMD_PLUGINS/alt-mysql';" "$PLUGIN_DIR/exec/lib/AppContext.php" \
|
grep -Fq "return '/CMD_PLUGINS/alt-mysql';" "$PLUGIN_DIR/exec/lib/AppContext.php" \
|
||||||
|| fail "AppContext base URL is not alt-mysql"
|
|| fail "AppContext base URL is not alt-mysql"
|
||||||
grep -Fq "\$pluginId = 'alt-mysql';" "$PLUGIN_DIR/exec/lib/DirectAdminUser.php" \
|
grep -Fq "\$pluginId = 'alt-mysql';" "$PLUGIN_DIR/exec/lib/DirectAdminUser.php" \
|
||||||
|
|||||||
Reference in New Issue
Block a user