Skip to content

Commit b93d1b9

Browse files
Merge pull request #19154 from MauricioFauth/auth-plugin-response-handling
Remove ResponseRenderer::callExit() calls from auth plugins
2 parents c6c9aeb + 6a425a2 commit b93d1b9

15 files changed

Lines changed: 475 additions & 377 deletions

docs/faq.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2235,9 +2235,10 @@ logs. Currently there are two variables available:
22352235
User name of currently active user (they do not have to be logged in).
22362236
``userStatus``
22372237
Status of currently active user, one of ``ok`` (user is logged in),
2238-
``mysql-denied`` (MySQL denied user login), ``allow-denied`` (user denied
2238+
``server-denied`` (database server denied user login), ``allow-denied`` (user denied
22392239
by allow/deny rules), ``root-denied`` (root is denied in configuration),
2240-
``empty-denied`` (empty password is denied).
2240+
``empty-denied`` (empty password is denied),
2241+
``no-activity`` (automatically logged out due to inactivity).
22412242

22422243
``LogFormat`` directive for Apache can look like following:
22432244

psalm-baseline.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7672,6 +7672,7 @@
76727672
<code><![CDATA[$password]]></code>
76737673
</PossiblyInvalidPropertyAssignmentValue>
76747674
<PossiblyUnusedReturnValue>
7675+
<code><![CDATA[Response]]></code>
76757676
<code><![CDATA[bool]]></code>
76767677
</PossiblyUnusedReturnValue>
76777678
<RedundantCast>
@@ -7761,6 +7762,9 @@
77617762
<code><![CDATA[$config->selectedServer]]></code>
77627763
<code><![CDATA[array_merge($config->selectedServer, $singleSignonCfgUpdate)]]></code>
77637764
</MixedPropertyTypeCoercion>
7765+
<PossiblyUnusedReturnValue>
7766+
<code><![CDATA[Response]]></code>
7767+
</PossiblyUnusedReturnValue>
77647768
<RiskyTruthyFalsyComparison>
77657769
<code><![CDATA[empty($config->selectedServer['SignonURL'])]]></code>
77667770
</RiskyTruthyFalsyComparison>
@@ -14310,6 +14314,7 @@
1431014314
<code><![CDATA[$config->settings]]></code>
1431114315
<code><![CDATA[$config->settings]]></code>
1431214316
<code><![CDATA[$config->settings]]></code>
14317+
<code><![CDATA[$config->settings]]></code>
1431314318
</PropertyTypeCoercion>
1431414319
<TypeDoesNotContainType>
1431514320
<code><![CDATA[assertSame]]></code>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMyAdmin\Exceptions;
6+
7+
use RuntimeException;
8+
use Throwable;
9+
10+
use function __;
11+
12+
final class AuthenticationFailure extends RuntimeException
13+
{
14+
public const SERVER_DENIED = 'server-denied';
15+
public const ALLOW_DENIED = 'allow-denied';
16+
public const ROOT_DENIED = 'root-denied';
17+
public const EMPTY_DENIED = 'empty-denied';
18+
public const NO_ACTIVITY = 'no-activity';
19+
20+
/** @psalm-param self::* $failureType */
21+
public function __construct(
22+
public readonly string $failureType,
23+
string $message = '',
24+
int $code = 0,
25+
Throwable|null $previous = null,
26+
) {
27+
parent::__construct($message, $code, $previous);
28+
}
29+
30+
/**
31+
* Database server denied user login
32+
*/
33+
public static function deniedByDatabaseServer(): self
34+
{
35+
return new self(self::SERVER_DENIED, __('Cannot log in to the database server.'));
36+
}
37+
38+
/**
39+
* User denied by allow/deny rules
40+
*/
41+
public static function deniedByAllowDenyRules(): self
42+
{
43+
return new self(self::ALLOW_DENIED, __('Access denied!'));
44+
}
45+
46+
/**
47+
* User 'root' is denied in configuration
48+
*/
49+
public static function rootDeniedByConfiguration(): self
50+
{
51+
return new self(self::ROOT_DENIED, __('Access denied!'));
52+
}
53+
54+
/**
55+
* Empty password is denied
56+
*/
57+
public static function emptyPasswordDeniedByConfiguration(): self
58+
{
59+
return new self(
60+
self::EMPTY_DENIED,
61+
__('Login without a password is forbidden by configuration (see AllowNoPassword).'),
62+
);
63+
}
64+
65+
/**
66+
* Automatically logged out due to inactivity
67+
*/
68+
public static function loggedOutDueToInactivity(): self
69+
{
70+
return new self(
71+
self::NO_ACTIVITY,
72+
__(
73+
'You have been automatically logged out due to inactivity of %s seconds.'
74+
. ' Once you log in again, you should be able to resume the work where you left off.',
75+
),
76+
);
77+
}
78+
}

src/Http/Middleware/Authentication.php

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
use PhpMyAdmin\Container\ContainerBuilder;
1212
use PhpMyAdmin\DatabaseInterface;
1313
use PhpMyAdmin\Dbal\ConnectionType;
14+
use PhpMyAdmin\Exceptions\AuthenticationFailure;
1415
use PhpMyAdmin\Exceptions\AuthenticationPluginException;
1516
use PhpMyAdmin\Exceptions\ExitException;
1617
use PhpMyAdmin\Http\Factory\ResponseFactory;
1718
use PhpMyAdmin\Http\ServerRequest;
1819
use PhpMyAdmin\LanguageManager;
1920
use PhpMyAdmin\Logging;
20-
use PhpMyAdmin\Plugins\AuthenticationPlugin;
2121
use PhpMyAdmin\Plugins\AuthenticationPluginFactory;
2222
use PhpMyAdmin\ResponseRenderer;
2323
use PhpMyAdmin\Template;
@@ -26,6 +26,7 @@
2626
use Psr\Http\Message\ServerRequestInterface;
2727
use Psr\Http\Server\MiddlewareInterface;
2828
use Psr\Http\Server\RequestHandlerInterface;
29+
use Throwable;
2930

3031
use function assert;
3132
use function define;
@@ -60,7 +61,23 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
6061
}
6162

6263
try {
63-
$authPlugin->authenticate();
64+
try {
65+
$response = $authPlugin->authenticate();
66+
if ($response !== null) {
67+
return $response;
68+
}
69+
} catch (AuthenticationFailure $exception) {
70+
return $authPlugin->showFailure($exception);
71+
} catch (Throwable $exception) {
72+
$response = $this->responseFactory->createResponse(StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
73+
74+
return $response->write($this->template->render('error/generic', [
75+
'lang' => $GLOBALS['lang'] ?? 'en',
76+
'dir' => LanguageManager::$textDir,
77+
'error_message' => $exception->getMessage(),
78+
]));
79+
}
80+
6481
$currentServer = new Server(Config::getInstance()->selectedServer);
6582

6683
/* Enable LOAD DATA LOCAL INFILE for LDI plugin */
@@ -71,7 +88,11 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
7188
// phpcs:enable
7289
}
7390

74-
$this->connectToDatabaseServer(DatabaseInterface::getInstance(), $authPlugin, $currentServer);
91+
try {
92+
$this->connectToDatabaseServer(DatabaseInterface::getInstance(), $currentServer);
93+
} catch (AuthenticationFailure $exception) {
94+
return $authPlugin->showFailure($exception);
95+
}
7596

7697
// Relation should only be initialized after the connection is successful
7798
/** @var Relation $relation */
@@ -81,9 +102,16 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
81102
// Tracker can only be activated after the relation has been initialized
82103
Tracker::enable();
83104

84-
$authPlugin->rememberCredentials();
105+
$response = $authPlugin->rememberCredentials();
106+
if ($response !== null) {
107+
return $response;
108+
}
109+
85110
assert($request instanceof ServerRequest);
86-
$authPlugin->checkTwoFactor($request);
111+
$response = $authPlugin->checkTwoFactor($request);
112+
if ($response !== null) {
113+
return $response;
114+
}
87115
} catch (ExitException) {
88116
return ResponseRenderer::getInstance()->response();
89117
}
@@ -94,11 +122,9 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
94122
return $handler->handle($request);
95123
}
96124

97-
private function connectToDatabaseServer(
98-
DatabaseInterface $dbi,
99-
AuthenticationPlugin $auth,
100-
Server $currentServer,
101-
): void {
125+
/** @throws AuthenticationFailure */
126+
private function connectToDatabaseServer(DatabaseInterface $dbi, Server $currentServer): void
127+
{
102128
/**
103129
* Try to connect MySQL with the control user profile (will be used to get the privileges list for the current
104130
* user but the true user link must be open after this one, so it would be default one for all the scripts).
@@ -111,7 +137,7 @@ private function connectToDatabaseServer(
111137
// Connects to the server (validates user's login)
112138
$userConnection = $dbi->connect($currentServer, ConnectionType::User);
113139
if ($userConnection === null) {
114-
$auth->showFailure('mysql-denied');
140+
throw AuthenticationFailure::deniedByDatabaseServer();
115141
}
116142

117143
if ($controlConnection !== null) {

src/Plugins/Auth/AuthenticationConfig.php

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@
1010
use PhpMyAdmin\Config;
1111
use PhpMyAdmin\DatabaseInterface;
1212
use PhpMyAdmin\Error\ErrorHandler;
13+
use PhpMyAdmin\Exceptions\AuthenticationFailure;
1314
use PhpMyAdmin\Html\Generator;
15+
use PhpMyAdmin\Http\Response;
1416
use PhpMyAdmin\Plugins\AuthenticationPlugin;
1517
use PhpMyAdmin\ResponseRenderer;
1618
use PhpMyAdmin\Server\Select;
1719
use PhpMyAdmin\Util;
1820

1921
use function __;
2022
use function count;
23+
use function ob_get_clean;
24+
use function ob_start;
2125
use function sprintf;
2226
use function trigger_error;
2327

@@ -32,17 +36,18 @@ class AuthenticationConfig extends AuthenticationPlugin
3236
/**
3337
* Displays authentication form
3438
*/
35-
public function showLoginForm(): void
39+
public function showLoginForm(): Response|null
3640
{
37-
$response = ResponseRenderer::getInstance();
38-
if (! $response->isAjax()) {
39-
return;
41+
$responseRenderer = ResponseRenderer::getInstance();
42+
if (! $responseRenderer->isAjax()) {
43+
return null;
4044
}
4145

42-
$response->setRequestStatus(false);
46+
$responseRenderer->setRequestStatus(false);
4347
// reload_flag removes the token parameter from the URL and reloads
44-
$response->addJSON('reload_flag', '1');
45-
$response->callExit();
48+
$responseRenderer->addJSON('reload_flag', '1');
49+
50+
return $responseRenderer->response();
4651
}
4752

4853
/**
@@ -65,25 +70,25 @@ public function readCredentials(): bool
6570

6671
/**
6772
* User is not allowed to login to MySQL -> authentication failed
68-
*
69-
* @param string $failure String describing why authentication has failed
7073
*/
71-
public function showFailure(string $failure): never
74+
public function showFailure(AuthenticationFailure $failure): Response
7275
{
73-
parent::showFailure($failure);
76+
$this->logFailure($failure);
7477

7578
$connError = DatabaseInterface::getInstance()->getError();
7679
if ($connError === '' || $connError === '0') {
7780
$connError = __('Cannot connect: invalid settings.');
7881
}
7982

8083
/* HTML header */
81-
$response = ResponseRenderer::getInstance();
82-
$response->setMinimalFooter();
83-
$header = $response->getHeader();
84+
$responseRenderer = ResponseRenderer::getInstance();
85+
$responseRenderer->setMinimalFooter();
86+
$header = $responseRenderer->getHeader();
8487
$header->setBodyId('loginform');
8588
$header->setTitle(__('Access denied!'));
8689
$header->disableMenuAndConsole();
90+
91+
ob_start();
8792
echo '<br><br>
8893
<div class="text-center">
8994
<h1>';
@@ -95,8 +100,8 @@ public function showFailure(string $failure): never
95100
<tr>
96101
<td>';
97102
$config = Config::getInstance();
98-
if ($failure === 'allow-denied') {
99-
trigger_error(__('Access denied!'), E_USER_NOTICE);
103+
if ($failure->failureType === AuthenticationFailure::ALLOW_DENIED) {
104+
trigger_error($failure->getMessage(), E_USER_NOTICE);
100105
} else {
101106
// Check whether user has configured something
102107
if ($config->sourceMtime == 0) {
@@ -158,6 +163,9 @@ public function showFailure(string $failure): never
158163
}
159164

160165
echo '</table>' , "\n";
161-
$response->callExit();
166+
167+
$responseRenderer->addHTML((string) ob_get_clean());
168+
169+
return $responseRenderer->response();
162170
}
163171
}

0 commit comments

Comments
 (0)