diff options
author | Roeland Jago Douma <rullzer@users.noreply.github.com> | 2018-06-21 11:09:20 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-06-21 11:09:20 +0300 |
commit | 8ebc3d90a0876d243c889108f3a95219c0863458 (patch) | |
tree | a7fce3b4416ab1c86f1704b67fedd72555c9b348 /lib | |
parent | f9c98d86212f14b006fdf99251e35093d5026e80 (diff) | |
parent | a596b062f520469ca52eed10a407fc4cf8891239 (diff) |
Merge pull request #9518 from nextcloud/feature/5986/public_share_controller_middleware
Public share middleware & controller
Diffstat (limited to 'lib')
8 files changed, 465 insertions, 2 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 77729886601..2426272886b 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 be9c71d8246..26a38a29984 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 c82ac5255dd..8803ef8c47d 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 00000000000..27e57fe9505 --- /dev/null +++ b/lib/private/AppFramework/Middleware/PublicShare/Exceptions/NeedAuthenticationException.php @@ -0,0 +1,7 @@ +<?php + +namespace OC\AppFramework\Middleware\PublicShare\Exceptions; + +class NeedAuthenticationException extends \Exception { + +} diff --git a/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php b/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php new file mode 100644 index 00000000000..05783b21900 --- /dev/null +++ b/lib/private/AppFramework/Middleware/PublicShare/PublicShareMiddleware.php @@ -0,0 +1,112 @@ +<?php + +namespace OC\AppFramework\Middleware\PublicShare; + +use OC\AppFramework\Middleware\PublicShare\Exceptions\NeedAuthenticationException; +use OCP\AppFramework\AuthPublicShareController; +use OCP\AppFramework\Http\NotFoundResponse; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; +use OCP\AppFramework\PublicShareController; +use OCP\Files\NotFoundException; +use OCP\IConfig; +use OCP\IRequest; +use OCP\ISession; + +class PublicShareMiddleware extends Middleware { + /** @var IRequest */ + private $request; + + /** @var ISession */ + private $session; + + /** @var IConfig */ + private $config; + + public function __construct(IRequest $request, ISession $session, IConfig $config) { + $this->request = $request; + $this->session = $session; + $this->config = $config; + } + + public function beforeController($controller, $methodName) { + if (!($controller instanceof PublicShareController)) { + return; + } + + if (!$this->isLinkSharingEnabled()) { + throw new NotFoundException('Link sharing is disabled'); + } + + // 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()) { + 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 { + $tmp = explode('.', $route); + return array_pop($tmp); + } + + /** + * Check if link sharing is allowed + */ + private function isLinkSharingEnabled(): bool { + // Check if the shareAPI is enabled + if ($this->config->getAppValue('core', 'shareapi_enabled', 'yes') !== 'yes') { + return false; + } + + // Check whether public sharing is enabled + if($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') { + return false; + } + + return true; + } +} diff --git a/lib/private/legacy/template/functions.php b/lib/private/legacy/template/functions.php index 290ffe120a3..55d3a595689 100644 --- a/lib/private/legacy/template/functions.php +++ b/lib/private/legacy/template/functions.php @@ -262,7 +262,7 @@ function preview_icon( $path ) { * @return string */ function publicPreview_icon ( $path, $token ) { - return \OC::$server->getURLGenerator()->linkToRoute('files_sharing.PublicPreview.getPreview', ['x' => 32, 'y' => 32, 'file' => $path, 't' => $token]); + return \OC::$server->getURLGenerator()->linkToRoute('files_sharing.PublicPreview.getPreview', ['x' => 32, 'y' => 32, 'file' => $path, 'token' => $token]); } /** diff --git a/lib/public/AppFramework/AuthPublicShareController.php b/lib/public/AppFramework/AuthPublicShareController.php new file mode 100644 index 00000000000..ffd2bddd24b --- /dev/null +++ b/lib/public/AppFramework/AuthPublicShareController.php @@ -0,0 +1,192 @@ +<?php +/** + * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @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/>. + * + */ +declare(strict_types=1); + +namespace OCP\AppFramework; + +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\TemplateResponse; +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 + */ + public function showAuthenticate(): TemplateResponse { + return new TemplateResponse('core', 'publicshareauth', [], 'guest'); + } + + /** + * The template to show when authentication failed + * + * @since 14.0.0 + */ + protected function showAuthFailed(): TemplateResponse { + return new TemplateResponse('core', 'publicshareauth', ['wrongpw' => true], 'guest'); + } + + /** + * 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(true, true); + $response = $this->getRedirect(); + + $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 = [ + 'token' => $this->getToken(), + ]; + } 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 00000000000..d0e54a0295b --- /dev/null +++ b/lib/public/AppFramework/PublicShareController.php @@ -0,0 +1,138 @@ +<?php +/** + * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @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/>. + * + */ +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 + */ + final 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() { + + } +} |