diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 7772988660..2426272886 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -18,6 +18,7 @@ return array( 'OCP\\App' => $baseDir . '/lib/public/App.php', 'OCP\\AppFramework\\ApiController' => $baseDir . '/lib/public/AppFramework/ApiController.php', 'OCP\\AppFramework\\App' => $baseDir . '/lib/public/AppFramework/App.php', + 'OCP\\AppFramework\\AuthPublicShareController' => $baseDir . '/lib/public/AppFramework/AuthPublicShareController.php', 'OCP\\AppFramework\\Controller' => $baseDir . '/lib/public/AppFramework/Controller.php', 'OCP\\AppFramework\\Db\\DoesNotExistException' => $baseDir . '/lib/public/AppFramework/Db/DoesNotExistException.php', 'OCP\\AppFramework\\Db\\Entity' => $baseDir . '/lib/public/AppFramework/Db/Entity.php', @@ -56,6 +57,7 @@ return array( 'OCP\\AppFramework\\OCS\\OCSException' => $baseDir . '/lib/public/AppFramework/OCS/OCSException.php', 'OCP\\AppFramework\\OCS\\OCSForbiddenException' => $baseDir . '/lib/public/AppFramework/OCS/OCSForbiddenException.php', 'OCP\\AppFramework\\OCS\\OCSNotFoundException' => $baseDir . '/lib/public/AppFramework/OCS/OCSNotFoundException.php', + 'OCP\\AppFramework\\PublicShareController' => $baseDir . '/lib/public/AppFramework/PublicShareController.php', 'OCP\\AppFramework\\QueryException' => $baseDir . '/lib/public/AppFramework/QueryException.php', 'OCP\\AppFramework\\Utility\\IControllerMethodReflector' => $baseDir . '/lib/public/AppFramework/Utility/IControllerMethodReflector.php', 'OCP\\AppFramework\\Utility\\ITimeFactory' => $baseDir . '/lib/public/AppFramework/Utility/ITimeFactory.php', @@ -350,6 +352,8 @@ return array( 'OC\\AppFramework\\Http\\Request' => $baseDir . '/lib/private/AppFramework/Http/Request.php', 'OC\\AppFramework\\Middleware\\MiddlewareDispatcher' => $baseDir . '/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php', 'OC\\AppFramework\\Middleware\\OCSMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/OCSMiddleware.php', + 'OC\\AppFramework\\Middleware\\PublicShare\\Exceptions\\NeedAuthenticationException' => $baseDir . '/lib/private/AppFramework/Middleware/PublicShare/Exceptions/NeedAuthenticationException.php', + 'OC\\AppFramework\\Middleware\\PublicShare\\PublicShareMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\BruteForceMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\CORSMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\AppNotEnabledException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index be9c71d824..26a38a2998 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -48,6 +48,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\App' => __DIR__ . '/../../..' . '/lib/public/App.php', 'OCP\\AppFramework\\ApiController' => __DIR__ . '/../../..' . '/lib/public/AppFramework/ApiController.php', 'OCP\\AppFramework\\App' => __DIR__ . '/../../..' . '/lib/public/AppFramework/App.php', + 'OCP\\AppFramework\\AuthPublicShareController' => __DIR__ . '/../../..' . '/lib/public/AppFramework/AuthPublicShareController.php', 'OCP\\AppFramework\\Controller' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Controller.php', 'OCP\\AppFramework\\Db\\DoesNotExistException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/DoesNotExistException.php', 'OCP\\AppFramework\\Db\\Entity' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/Entity.php', @@ -86,6 +87,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\AppFramework\\OCS\\OCSException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/OCS/OCSException.php', 'OCP\\AppFramework\\OCS\\OCSForbiddenException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/OCS/OCSForbiddenException.php', 'OCP\\AppFramework\\OCS\\OCSNotFoundException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/OCS/OCSNotFoundException.php', + 'OCP\\AppFramework\\PublicShareController' => __DIR__ . '/../../..' . '/lib/public/AppFramework/PublicShareController.php', 'OCP\\AppFramework\\QueryException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/QueryException.php', 'OCP\\AppFramework\\Utility\\IControllerMethodReflector' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Utility/IControllerMethodReflector.php', 'OCP\\AppFramework\\Utility\\ITimeFactory' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Utility/ITimeFactory.php', @@ -380,6 +382,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\AppFramework\\Http\\Request' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http/Request.php', 'OC\\AppFramework\\Middleware\\MiddlewareDispatcher' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php', 'OC\\AppFramework\\Middleware\\OCSMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/OCSMiddleware.php', + 'OC\\AppFramework\\Middleware\\PublicShare\\Exceptions\\NeedAuthenticationException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/PublicShare/Exceptions/NeedAuthenticationException.php', + 'OC\\AppFramework\\Middleware\\PublicShare\\PublicShareMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\BruteForceMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\CORSMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\AppNotEnabledException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php', diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index c82ac5255d..8803ef8c47 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -62,6 +62,7 @@ use OCP\IL10N; use OCP\ILogger; use OCP\IRequest; use OCP\IServerContainer; +use OCP\ISession; use OCP\IUserSession; use OCP\RichObjectStrings\IValidator; use OCP\Encryption\IManager; @@ -304,7 +305,7 @@ class DIContainer extends SimpleContainer implements IAppContainer { }); $middleWares = &$this->middleWares; - $this->registerService('MiddlewareDispatcher', function($c) use (&$middleWares) { + $this->registerService('MiddlewareDispatcher', function(SimpleContainer $c) use (&$middleWares) { $dispatcher = new MiddlewareDispatcher(); $dispatcher->registerMiddleware($c[OC\AppFramework\Middleware\Security\SameSiteCookieMiddleware::class]); $dispatcher->registerMiddleware($c['CORSMiddleware']); @@ -314,6 +315,11 @@ class DIContainer extends SimpleContainer implements IAppContainer { $dispatcher->registerMiddleware($c['TwoFactorMiddleware']); $dispatcher->registerMiddleware($c['BruteForceMiddleware']); $dispatcher->registerMiddleware($c['RateLimitingMiddleware']); + $dispatcher->registerMiddleware(new OC\AppFramework\Middleware\PublicShare\PublicShareMiddleware( + $c['Request'], + $c->query(ISession::class), + $c->query(\OCP\IConfig::class) + )); foreach($middleWares as $middleWare) { $dispatcher->registerMiddleware($c[$middleWare]); diff --git a/lib/private/AppFramework/Middleware/PublicShare/Exceptions/NeedAuthenticationException.php b/lib/private/AppFramework/Middleware/PublicShare/Exceptions/NeedAuthenticationException.php new file mode 100644 index 0000000000..27e57fe950 --- /dev/null +++ b/lib/private/AppFramework/Middleware/PublicShare/Exceptions/NeedAuthenticationException.php @@ -0,0 +1,7 @@ +request = $request; + $this->session = $session; + } + + public function beforeController($controller, $methodName) { + if (!($controller instanceof PublicShareController)) { + return; + } + + // We require the token parameter to be set + $token = $this->request->getParam('token'); + if ($token === null) { + throw new NotFoundException(); + } + + // Set the token + $controller->setToken($token); + + if (!$controller->isValidToken()) { + $controller->shareNotFound(); + throw new NotFoundException(); + } + + // No need to check for authentication when we try to authenticate + if ($methodName === 'authenticate' || $methodName === 'showAuthenticate') { + return; + } + + // If authentication succeeds just continue + if ($controller->isAuthenticated($token)) { + return; + } + + // If we can authenticate to this controller do it else we throw a 404 to not leak any info + if ($controller instanceof AuthPublicShareController) { + $this->session->set('public_link_authenticate_redirect', json_encode($this->request->getParams())); + throw new NeedAuthenticationException(); + } + + throw new NotFoundException(); + + } + + public function afterException($controller, $methodName, \Exception $exception) { + if (!($controller instanceof PublicShareController)) { + throw $exception; + } + + if ($exception instanceof NotFoundException) { + return new NotFoundResponse(); + } + + if ($controller instanceof AuthPublicShareController && $exception instanceof NeedAuthenticationException) { + return $controller->getAuthenticationRedirect($this->getFunctionForRoute($this->request->getParam('_route'))); + } + + throw $exception; + } + + private function getFunctionForRoute(string $route): string { + return array_pop(explode('.', $route)); + } +} diff --git a/lib/public/AppFramework/AuthPublicShareController.php b/lib/public/AppFramework/AuthPublicShareController.php new file mode 100644 index 0000000000..7740faa585 --- /dev/null +++ b/lib/public/AppFramework/AuthPublicShareController.php @@ -0,0 +1,188 @@ + + * + * @author Roeland Jago Douma + * + * @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 . + * + */ +declare(strict_types=1); + +namespace OCP\AppFramework; + +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\Files\NotFoundException; +use OCP\IRequest; +use OCP\ISession; +use OCP\IURLGenerator; + +/** + * Base controller for interactive public shares + * + * It will verify if the user is properly authenticated to the share. If not the + * user will be redirected to an authentication page. + * + * Use this for a controller that is to be called directly by a user. So the + * normal public share page for files/calendars etc. + * + * @since 14.0.0 + */ +abstract class AuthPublicShareController extends PublicShareController { + + /** @var IURLGenerator */ + protected $urlGenerator; + + /** + * @since 14.0.0 + */ + public function __construct(string $appName, + IRequest $request, + ISession $session, + IURLGenerator $urlGenerator) { + parent::__construct($appName, $request, $session); + + $this->urlGenerator = $urlGenerator; + } + + /** + * @PublicPage + * @NoCSRFRequired + * + * Show the authentication page + * The form has to submit to the authenticate method route + * + * @since 14.0.0 + */ + abstract public function showAuthenticate(): TemplateResponse; + + /** + * The template to show when authentication failed + * + * @since 14.0.0 + */ + abstract protected function showAuthFailed(): TemplateResponse; + + /** + * Verify the password + * + * @since 14.0.0 + */ + abstract protected function verifyPassword(string $password): bool; + + /** + * Function called after failed authentication + * + * You can use this to do some logging for example + * + * @since 14.0.0 + */ + protected function authFailed() { + } + + /** + * Function called after successfull authentication + * + * You can use this to do some logging for example + * + * @since 14.0.0 + */ + protected function authSucceeded() { + } + + /** + * @UseSession + * @PublicPage + * @BruteForceProtection(action=publicLinkAuth) + * + * Authenticate the share + * + * @since 14.0.0 + */ + final public function authenticate(string $password = '') { + // Already authenticated + if ($this->isAuthenticated()) { + return $this->getRedirect(); + } + + if (!$this->verifyPassword($password)) { + $this->authFailed(); + $response = $this->showAuthFailed(); + $response->throttle(); + return $response; + } + + $this->session->regenerateId(); + $response = $this->getRedirect(); + + $this->session->clear(); + $this->session->set('public_link_authenticated_token', $this->getToken()); + $this->session->set('public_link_authenticated_password_hash', $this->getPasswordHash()); + + $this->authSucceeded(); + + return $response; + } + + /** + * Default landing page + * + * @since 14.0.0 + */ + abstract public function showShare(): TemplateResponse; + + /** + * @since 14.0.0 + */ + final public function getAuthenticationRedirect(string $redirect): RedirectResponse { + return new RedirectResponse( + $this->urlGenerator->linkToRoute($this->getRoute('showAuthenticate'), ['token' => $this->getToken(), 'redirect' => $redirect]) + ); + } + + /** + * @since 14.0.0 + */ + private function getRoute(string $function): string { + $app = strtolower($this->appName); + $class = strtolower((new \ReflectionClass($this))->getShortName()); + + return $app . '.' . $class . '.' . $function; + } + + /** + * @since 14.0.0 + */ + private function getRedirect(): RedirectResponse { + //Get all the stored redirect parameters: + $params = $this->session->get('public_link_authenticate_redirect'); + + $route = $this->getRoute('showShare'); + + if ($params === null) { + $params = []; + } else { + $params = json_decode($params, true); + if (isset($params['_route'])) { + $route = $params['_route']; + unset($params['_route']); + } + } + + return new RedirectResponse($this->urlGenerator->linkToRoute($route, $params)); + } +} diff --git a/lib/public/AppFramework/PublicShareController.php b/lib/public/AppFramework/PublicShareController.php new file mode 100644 index 0000000000..64fbe9df40 --- /dev/null +++ b/lib/public/AppFramework/PublicShareController.php @@ -0,0 +1,138 @@ + + * + * @author Roeland Jago Douma + * + * @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 . + * + */ +declare(strict_types=1); + +namespace OCP\AppFramework; + +use OCP\IRequest; +use OCP\ISession; + +/** + * Base controller for public shares + * + * It will verify if the user is properly authenticated to the share. If not a 404 + * is thrown by the PublicShareMiddleware. + * + * Use this for example for a controller that is not to be called via a webbrowser + * directly. For example a PublicPreviewController. As this is not meant to be + * called by a user direclty. + * + * To show an auth page extend the AuthPublicShareController + * + * @since 14.0.0 + */ +abstract class PublicShareController extends Controller { + + /** @var ISession */ + protected $session; + + /** @var string */ + private $token; + + /** + * @since 14.0.0 + */ + public function __construct(string $appName, + IRequest $request, + ISession $session) { + parent::__construct($appName, $request); + + $this->session = $session; + } + + /** + * Middleware set the token for the request + * + * @since 14.0.0 + */ + final public function setToken(string $token) { + $this->token = $token; + } + + /** + * Get the token for this request + * + * @since 14.0.0 + */ + public function getToken(): string { + return $this->token; + } + + /** + * Get a hash of the password for this share + * + * To ensure access is blocked when the password to a share is changed we store + * a hash of the password for this token. + * + * @since 14.0.0 + */ + abstract protected function getPasswordHash(): string; + + /** + * Is the provided token a valid token + * + * This function is already called from the middleware directly after setting the token. + * + * @since 14.0.0 + */ + abstract public function isValidToken(): bool; + + /** + * Is a share with this token password protected + * + * @since 14.0.0 + */ + abstract protected function isPasswordProtected(): bool; + + /** + * Check if a share is authenticated or not + * + * @since 14.0.0 + */ + final public function isAuthenticated(): bool { + // Always authenticated against non password protected shares + if (!$this->isPasswordProtected()) { + return true; + } + + // If we are authenticated properly + if ($this->session->get('public_link_authenticated_token') === $this->getToken() && + $this->session->get('public_link_authenticated_password_hash') === $this->getPasswordHash()) { + return true; + } + + // Fail by default if nothing matches + return false; + } + + /** + * Function called if the share is not found. + * + * You can use this to do some logging for example + * + * @since 14.0.0 + */ + public function shareNotFound() { + + } +}