Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>tags/v26.0.0beta2
@@ -41,6 +41,7 @@ use OCA\OAuth2\Db\AccessTokenMapper; | |||
use OCA\OAuth2\Db\ClientMapper; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http; | |||
use OCP\AppFramework\Http\Attribute\UseSession; | |||
use OCP\AppFramework\Http\Response; | |||
use OCP\AppFramework\Http\StandaloneTemplateResponse; | |||
use OCP\Defaults; | |||
@@ -126,8 +127,8 @@ class ClientFlowLoginController extends Controller { | |||
/** | |||
* @PublicPage | |||
* @NoCSRFRequired | |||
* @UseSession | |||
*/ | |||
#[UseSession] | |||
public function showAuthPickerPage(string $clientIdentifier = '', string $user = '', int $direct = 0): StandaloneTemplateResponse { | |||
$clientName = $this->getClientName(); | |||
$client = null; | |||
@@ -193,8 +194,8 @@ class ClientFlowLoginController extends Controller { | |||
* @NoAdminRequired | |||
* @NoCSRFRequired | |||
* @NoSameSiteCookieRequired | |||
* @UseSession | |||
*/ | |||
#[UseSession] | |||
public function grantPage(string $stateToken = '', | |||
string $clientIdentifier = '', | |||
int $direct = 0): StandaloneTemplateResponse { | |||
@@ -243,10 +244,10 @@ class ClientFlowLoginController extends Controller { | |||
/** | |||
* @NoAdminRequired | |||
* @UseSession | |||
* | |||
* @return Http\RedirectResponse|Response | |||
*/ | |||
#[UseSession] | |||
public function generateAppPassword(string $stateToken, | |||
string $clientIdentifier = '') { | |||
if (!$this->isValidToken($stateToken)) { |
@@ -33,6 +33,7 @@ use OC\Core\Exception\LoginFlowV2NotFoundException; | |||
use OC\Core\Service\LoginFlowV2Service; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http; | |||
use OCP\AppFramework\Http\Attribute\UseSession; | |||
use OCP\AppFramework\Http\JSONResponse; | |||
use OCP\AppFramework\Http\RedirectResponse; | |||
use OCP\AppFramework\Http\Response; | |||
@@ -97,8 +98,8 @@ class ClientFlowLoginV2Controller extends Controller { | |||
/** | |||
* @NoCSRFRequired | |||
* @PublicPage | |||
* @UseSession | |||
*/ | |||
#[UseSession] | |||
public function landing(string $token, $user = ''): Response { | |||
if (!$this->loginFlowV2Service->startLoginFlow($token)) { | |||
return $this->loginTokenForbiddenResponse(); | |||
@@ -114,8 +115,8 @@ class ClientFlowLoginV2Controller extends Controller { | |||
/** | |||
* @NoCSRFRequired | |||
* @PublicPage | |||
* @UseSession | |||
*/ | |||
#[UseSession] | |||
public function showAuthPickerPage($user = ''): StandaloneTemplateResponse { | |||
try { | |||
$flow = $this->getFlowByLoginToken(); | |||
@@ -145,10 +146,10 @@ class ClientFlowLoginV2Controller extends Controller { | |||
/** | |||
* @NoAdminRequired | |||
* @UseSession | |||
* @NoCSRFRequired | |||
* @NoSameSiteCookieRequired | |||
*/ | |||
#[UseSession] | |||
public function grantPage(string $stateToken): StandaloneTemplateResponse { | |||
if (!$this->isValidStateToken($stateToken)) { | |||
return $this->stateTokenForbiddenResponse(); | |||
@@ -222,8 +223,8 @@ class ClientFlowLoginV2Controller extends Controller { | |||
/** | |||
* @NoAdminRequired | |||
* @UseSession | |||
*/ | |||
#[UseSession] | |||
public function generateAppPassword(string $stateToken): Response { | |||
if (!$this->isValidStateToken($stateToken)) { | |||
return $this->stateTokenForbiddenResponse(); |
@@ -43,6 +43,7 @@ use OC\User\Session; | |||
use OC_App; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http; | |||
use OCP\AppFramework\Http\Attribute\UseSession; | |||
use OCP\AppFramework\Http\DataResponse; | |||
use OCP\AppFramework\Http\RedirectResponse; | |||
use OCP\AppFramework\Http\TemplateResponse; | |||
@@ -105,10 +106,10 @@ class LoginController extends Controller { | |||
/** | |||
* @NoAdminRequired | |||
* @UseSession | |||
* | |||
* @return RedirectResponse | |||
*/ | |||
#[UseSession] | |||
public function logout() { | |||
$loginToken = $this->request->getCookie('nc_token'); | |||
if (!is_null($loginToken)) { | |||
@@ -134,13 +135,13 @@ class LoginController extends Controller { | |||
/** | |||
* @PublicPage | |||
* @NoCSRFRequired | |||
* @UseSession | |||
* | |||
* @param string $user | |||
* @param string $redirect_url | |||
* | |||
* @return TemplateResponse|RedirectResponse | |||
*/ | |||
#[UseSession] | |||
public function showLoginForm(string $user = null, string $redirect_url = null): Http\Response { | |||
if ($this->userSession->isLoggedIn()) { | |||
return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl()); | |||
@@ -283,12 +284,12 @@ class LoginController extends Controller { | |||
/** | |||
* @PublicPage | |||
* @UseSession | |||
* @NoCSRFRequired | |||
* @BruteForceProtection(action=login) | |||
* | |||
* @return RedirectResponse | |||
*/ | |||
#[UseSession] | |||
public function tryLogin(Chain $loginChain, | |||
string $user, | |||
string $password, | |||
@@ -368,12 +369,12 @@ class LoginController extends Controller { | |||
/** | |||
* @NoAdminRequired | |||
* @UseSession | |||
* @BruteForceProtection(action=sudo) | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
*/ | |||
#[UseSession] | |||
public function confirmPassword(string $password): DataResponse { | |||
$loginName = $this->userSession->getLoginName(); | |||
$loginResult = $this->userManager->checkPassword($loginName, $password); |
@@ -28,6 +28,7 @@ namespace OC\Core\Controller; | |||
use OC\Authentication\TwoFactorAuth\Manager; | |||
use OC_User; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http\Attribute\UseSession; | |||
use OCP\AppFramework\Http\RedirectResponse; | |||
use OCP\AppFramework\Http\StandaloneTemplateResponse; | |||
use OCP\Authentication\TwoFactorAuth\IActivatableAtLogin; | |||
@@ -110,13 +111,13 @@ class TwoFactorChallengeController extends Controller { | |||
/** | |||
* @NoAdminRequired | |||
* @NoCSRFRequired | |||
* @UseSession | |||
* @TwoFactorSetUpDoneRequired | |||
* | |||
* @param string $challengeProviderId | |||
* @param string $redirect_url | |||
* @return StandaloneTemplateResponse|RedirectResponse | |||
*/ | |||
#[UseSession] | |||
public function showChallenge($challengeProviderId, $redirect_url) { | |||
$user = $this->userSession->getUser(); | |||
$providerSet = $this->twoFactorManager->getProviderSet($user); | |||
@@ -161,7 +162,6 @@ class TwoFactorChallengeController extends Controller { | |||
/** | |||
* @NoAdminRequired | |||
* @NoCSRFRequired | |||
* @UseSession | |||
* @TwoFactorSetUpDoneRequired | |||
* | |||
* @UserRateThrottle(limit=5, period=100) | |||
@@ -171,6 +171,7 @@ class TwoFactorChallengeController extends Controller { | |||
* @param string $redirect_url | |||
* @return RedirectResponse | |||
*/ | |||
#[UseSession] | |||
public function solveChallenge($challengeProviderId, $challenge, $redirect_url = null) { | |||
$user = $this->userSession->getUser(); | |||
$provider = $this->twoFactorManager->getProvider($user, $challengeProviderId); |
@@ -33,6 +33,7 @@ use OC\Authentication\WebAuthn\Manager; | |||
use OC\URLGenerator; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http; | |||
use OCP\AppFramework\Http\Attribute\UseSession; | |||
use OCP\AppFramework\Http\JSONResponse; | |||
use OCP\IRequest; | |||
use OCP\ISession; | |||
@@ -63,8 +64,8 @@ class WebAuthnController extends Controller { | |||
/** | |||
* @NoAdminRequired | |||
* @PublicPage | |||
* @UseSession | |||
*/ | |||
#[UseSession] | |||
public function startAuthentication(string $loginName): JSONResponse { | |||
$this->logger->debug('Starting WebAuthn login'); | |||
@@ -87,8 +88,8 @@ class WebAuthnController extends Controller { | |||
/** | |||
* @NoAdminRequired | |||
* @PublicPage | |||
* @UseSession | |||
*/ | |||
#[UseSession] | |||
public function finishAuthentication(string $data): JSONResponse { | |||
$this->logger->debug('Validating WebAuthn login'); | |||
@@ -35,6 +35,7 @@ return array( | |||
'OCP\\AppFramework\\Db\\QBMapper' => $baseDir . '/lib/public/AppFramework/Db/QBMapper.php', | |||
'OCP\\AppFramework\\Db\\TTransactional' => $baseDir . '/lib/public/AppFramework/Db/TTransactional.php', | |||
'OCP\\AppFramework\\Http' => $baseDir . '/lib/public/AppFramework/Http.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\UseSession' => $baseDir . '/lib/public/AppFramework/Http/Attribute/UseSession.php', | |||
'OCP\\AppFramework\\Http\\ContentSecurityPolicy' => $baseDir . '/lib/public/AppFramework/Http/ContentSecurityPolicy.php', | |||
'OCP\\AppFramework\\Http\\DataDisplayResponse' => $baseDir . '/lib/public/AppFramework/Http/DataDisplayResponse.php', | |||
'OCP\\AppFramework\\Http\\DataDownloadResponse' => $baseDir . '/lib/public/AppFramework/Http/DataDownloadResponse.php', |
@@ -68,6 +68,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 | |||
'OCP\\AppFramework\\Db\\QBMapper' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/QBMapper.php', | |||
'OCP\\AppFramework\\Db\\TTransactional' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/TTransactional.php', | |||
'OCP\\AppFramework\\Http' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http.php', | |||
'OCP\\AppFramework\\Http\\Attribute\\UseSession' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/UseSession.php', | |||
'OCP\\AppFramework\\Http\\ContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/ContentSecurityPolicy.php', | |||
'OCP\\AppFramework\\Http\\DataDisplayResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/DataDisplayResponse.php', | |||
'OCP\\AppFramework\\Http\\DataDownloadResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/DataDownloadResponse.php', |
@@ -1,4 +1,7 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* @copyright Copyright (c) 2016, ownCloud, Inc. | |||
* | |||
@@ -27,9 +30,11 @@ namespace OC\AppFramework\Middleware; | |||
use OC\AppFramework\Utility\ControllerMethodReflector; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http\Attribute\UseSession; | |||
use OCP\AppFramework\Http\Response; | |||
use OCP\AppFramework\Middleware; | |||
use OCP\ISession; | |||
use ReflectionMethod; | |||
class SessionMiddleware extends Middleware { | |||
/** @var ControllerMethodReflector */ | |||
@@ -49,8 +54,18 @@ class SessionMiddleware extends Middleware { | |||
* @param string $methodName | |||
*/ | |||
public function beforeController($controller, $methodName) { | |||
$useSession = $this->reflector->hasAnnotation('UseSession'); | |||
if ($useSession) { | |||
/** | |||
* Annotation deprecated with Nextcloud 26 | |||
*/ | |||
$hasAnnotation = $this->reflector->hasAnnotation('UseSession'); | |||
if ($hasAnnotation) { | |||
$this->session->reopen(); | |||
return; | |||
} | |||
$reflectionMethod = new ReflectionMethod($controller, $methodName); | |||
$hasAttribute = !empty($reflectionMethod->getAttributes(UseSession::class)); | |||
if ($hasAttribute) { | |||
$this->session->reopen(); | |||
} | |||
} | |||
@@ -62,10 +77,21 @@ class SessionMiddleware extends Middleware { | |||
* @return Response | |||
*/ | |||
public function afterController($controller, $methodName, Response $response) { | |||
$useSession = $this->reflector->hasAnnotation('UseSession'); | |||
if ($useSession) { | |||
/** | |||
* Annotation deprecated with Nextcloud 26 | |||
*/ | |||
$hasAnnotation = $this->reflector->hasAnnotation('UseSession'); | |||
if ($hasAnnotation) { | |||
$this->session->close(); | |||
return $response; | |||
} | |||
$reflectionMethod = new ReflectionMethod($controller, $methodName); | |||
$hasAttribute = !empty($reflectionMethod->getAttributes(UseSession::class)); | |||
if ($hasAttribute) { | |||
$this->session->close(); | |||
} | |||
return $response; | |||
} | |||
} |
@@ -0,0 +1,37 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/* | |||
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\AppFramework\Http\Attribute; | |||
use Attribute; | |||
/** | |||
* Attribute for controller methods that need to read/write PHP session data | |||
* | |||
* @since 26.0.0 | |||
*/ | |||
#[Attribute] | |||
class UseSession { | |||
} |
@@ -1,4 +1,7 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/** | |||
* ownCloud - App Framework | |||
* | |||
@@ -14,84 +17,128 @@ namespace Test\AppFramework\Middleware; | |||
use OC\AppFramework\Middleware\SessionMiddleware; | |||
use OC\AppFramework\Utility\ControllerMethodReflector; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http\Attribute\UseSession; | |||
use OCP\AppFramework\Http\Response; | |||
use OCP\IRequest; | |||
use OCP\ISession; | |||
use PHPUnit\Framework\MockObject\MockObject; | |||
use Test\TestCase; | |||
class SessionMiddlewareTest extends \Test\TestCase { | |||
/** @var ControllerMethodReflector */ | |||
private $reflector; | |||
/** @var Controller */ | |||
private $controller; | |||
class SessionMiddlewareTest extends TestCase { | |||
private ControllerMethodReflector|MockObject $reflector; | |||
private ISession|MockObject $session; | |||
private Controller $controller; | |||
private SessionMiddleware $middleware; | |||
protected function setUp(): void { | |||
parent::setUp(); | |||
$this->reflector = new ControllerMethodReflector(); | |||
$this->controller = $this->createMock(Controller::class); | |||
$this->reflector = $this->createMock(ControllerMethodReflector::class); | |||
$this->session = $this->createMock(ISession::class); | |||
$this->controller = new class('app', $this->createMock(IRequest::class)) extends Controller { | |||
/** | |||
* @UseSession | |||
*/ | |||
public function withAnnotation() { | |||
} | |||
#[UseSession] | |||
public function withAttribute() { | |||
} | |||
public function without() { | |||
} | |||
}; | |||
$this->middleware = new SessionMiddleware( | |||
$this->reflector, | |||
$this->session, | |||
); | |||
} | |||
public function testSessionNotClosedOnBeforeController(): void { | |||
$this->configureSessionMock(0, 1); | |||
$this->reflector->expects(self::once()) | |||
->method('hasAnnotation') | |||
->with('UseSession') | |||
->willReturn(true); | |||
$this->middleware->beforeController($this->controller, 'withAnnotation'); | |||
} | |||
/** | |||
* @UseSession | |||
*/ | |||
public function testSessionNotClosedOnBeforeController() { | |||
$session = $this->getSessionMock(0, 1); | |||
public function testSessionNotClosedOnBeforeControllerWithAttribute(): void { | |||
$this->configureSessionMock(0, 1); | |||
$this->reflector->expects(self::once()) | |||
->method('hasAnnotation') | |||
->with('UseSession') | |||
->willReturn(false); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$middleware = new SessionMiddleware($this->reflector, $session); | |||
$middleware->beforeController($this->controller, __FUNCTION__); | |||
$this->middleware->beforeController($this->controller, 'withAttribute'); | |||
} | |||
/** | |||
* @UseSession | |||
*/ | |||
public function testSessionClosedOnAfterController() { | |||
$session = $this->getSessionMock(1); | |||
public function testSessionClosedOnAfterController(): void { | |||
$this->configureSessionMock(1); | |||
$this->reflector->expects(self::once()) | |||
->method('hasAnnotation') | |||
->with('UseSession') | |||
->willReturn(true); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$middleware = new SessionMiddleware($this->reflector, $session); | |||
$middleware->afterController($this->controller, __FUNCTION__, new Response()); | |||
$this->middleware->afterController($this->controller, 'withAnnotation', new Response()); | |||
} | |||
/** | |||
* @UseSession | |||
*/ | |||
public function testSessionReopenedAndClosedOnBeforeController() { | |||
$session = $this->getSessionMock(1, 1); | |||
public function testSessionClosedOnAfterControllerWithAttribute(): void { | |||
$this->configureSessionMock(1); | |||
$this->reflector->expects(self::once()) | |||
->method('hasAnnotation') | |||
->with('UseSession') | |||
->willReturn(true); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$middleware = new SessionMiddleware($this->reflector, $session); | |||
$middleware->beforeController($this->controller, __FUNCTION__); | |||
$middleware->afterController($this->controller, __FUNCTION__, new Response()); | |||
$this->middleware->afterController($this->controller, 'withAttribute', new Response()); | |||
} | |||
public function testSessionClosedOnBeforeController() { | |||
$session = $this->getSessionMock(0); | |||
public function testSessionReopenedAndClosedOnBeforeController(): void { | |||
$this->configureSessionMock(1, 1); | |||
$this->reflector->expects(self::exactly(2)) | |||
->method('hasAnnotation') | |||
->with('UseSession') | |||
->willReturn(true); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$middleware = new SessionMiddleware($this->reflector, $session); | |||
$middleware->beforeController($this->controller, __FUNCTION__); | |||
$this->middleware->beforeController($this->controller, 'withAnnotation'); | |||
$this->middleware->afterController($this->controller, 'withAnnotation', new Response()); | |||
} | |||
public function testSessionNotClosedOnAfterController() { | |||
$session = $this->getSessionMock(0); | |||
public function testSessionReopenedAndClosedOnBeforeControllerWithAttribute(): void { | |||
$this->configureSessionMock(1, 1); | |||
$this->reflector->expects(self::exactly(2)) | |||
->method('hasAnnotation') | |||
->with('UseSession') | |||
->willReturn(false); | |||
$this->reflector->reflect($this, __FUNCTION__); | |||
$middleware = new SessionMiddleware($this->reflector, $session); | |||
$middleware->afterController($this->controller, __FUNCTION__, new Response()); | |||
$this->middleware->beforeController($this->controller, 'withAttribute'); | |||
$this->middleware->afterController($this->controller, 'withAttribute', new Response()); | |||
} | |||
/** | |||
* @return mixed | |||
*/ | |||
private function getSessionMock(int $expectedCloseCount, int $expectedReopenCount = 0) { | |||
$session = $this->getMockBuilder('\OC\Session\Memory') | |||
->disableOriginalConstructor() | |||
->getMock(); | |||
public function testSessionClosedOnBeforeController(): void { | |||
$this->configureSessionMock(0); | |||
$this->reflector->expects(self::once()) | |||
->method('hasAnnotation') | |||
->with('UseSession') | |||
->willReturn(false); | |||
$this->middleware->beforeController($this->controller, 'without'); | |||
} | |||
public function testSessionNotClosedOnAfterController(): void { | |||
$this->configureSessionMock(0); | |||
$this->reflector->expects(self::once()) | |||
->method('hasAnnotation') | |||
->with('UseSession') | |||
->willReturn(false); | |||
$this->middleware->afterController($this->controller, 'without', new Response()); | |||
} | |||
$session->expects($this->exactly($expectedCloseCount)) | |||
private function configureSessionMock(int $expectedCloseCount, int $expectedReopenCount = 0): void { | |||
$this->session->expects($this->exactly($expectedCloseCount)) | |||
->method('close'); | |||
$session->expects($this->exactly($expectedReopenCount)) | |||
$this->session->expects($this->exactly($expectedReopenCount)) | |||
->method('reopen'); | |||
return $session; | |||
} | |||
} |