summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorprovokateurin <kate@provokateurin.de>2024-06-07 11:34:40 +0200
committerprovokateurin <kate@provokateurin.de>2024-07-01 14:41:20 +0200
commit5aefdc399eb17a86f3c2b59713ca6448479f99fd (patch)
tree2da8269d8669db9ee05c677a0d8c59e1866994ce
parentbeececf66068f57c416225efcde9b44ce5c2e835 (diff)
downloadnextcloud-server-5aefdc399eb17a86f3c2b59713ca6448479f99fd.tar.gz
nextcloud-server-5aefdc399eb17a86f3c2b59713ca6448479f99fd.zip
feat(AppFramework): Add ExAppRequired attribute
Signed-off-by: provokateurin <kate@provokateurin.de>
-rw-r--r--lib/composer/composer/autoload_classmap.php2
-rw-r--r--lib/composer/composer/autoload_static.php2
-rw-r--r--lib/private/AppFramework/Middleware/Security/Exceptions/ExAppRequiredException.php18
-rw-r--r--lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php10
-rw-r--r--lib/public/AppFramework/Http/Attribute/ExAppRequired.php21
-rw-r--r--tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php11
-rw-r--r--tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php48
7 files changed, 110 insertions, 2 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index ae747f8d248..3dcd614de21 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -47,6 +47,7 @@ return array(
'OCP\\AppFramework\\Http\\Attribute\\AuthorizedAdminSetting' => $baseDir . '/lib/public/AppFramework/Http/Attribute/AuthorizedAdminSetting.php',
'OCP\\AppFramework\\Http\\Attribute\\BruteForceProtection' => $baseDir . '/lib/public/AppFramework/Http/Attribute/BruteForceProtection.php',
'OCP\\AppFramework\\Http\\Attribute\\CORS' => $baseDir . '/lib/public/AppFramework/Http/Attribute/CORS.php',
+ 'OCP\\AppFramework\\Http\\Attribute\\ExAppRequired' => $baseDir . '/lib/public/AppFramework/Http/Attribute/ExAppRequired.php',
'OCP\\AppFramework\\Http\\Attribute\\FrontpageRoute' => $baseDir . '/lib/public/AppFramework/Http/Attribute/FrontpageRoute.php',
'OCP\\AppFramework\\Http\\Attribute\\IgnoreOpenAPI' => $baseDir . '/lib/public/AppFramework/Http/Attribute/IgnoreOpenAPI.php',
'OCP\\AppFramework\\Http\\Attribute\\NoAdminRequired' => $baseDir . '/lib/public/AppFramework/Http/Attribute/NoAdminRequired.php',
@@ -887,6 +888,7 @@ return array(
'OC\\AppFramework\\Middleware\\Security\\CSPMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php',
'OC\\AppFramework\\Middleware\\Security\\Exceptions\\AppNotEnabledException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php',
'OC\\AppFramework\\Middleware\\Security\\Exceptions\\CrossSiteRequestForgeryException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/CrossSiteRequestForgeryException.php',
+ 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\ExAppRequiredException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/ExAppRequiredException.php',
'OC\\AppFramework\\Middleware\\Security\\Exceptions\\LaxSameSiteCookieFailedException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/LaxSameSiteCookieFailedException.php',
'OC\\AppFramework\\Middleware\\Security\\Exceptions\\NotAdminException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/NotAdminException.php',
'OC\\AppFramework\\Middleware\\Security\\Exceptions\\NotConfirmedException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/NotConfirmedException.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 45393c5481e..493708f2e39 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -80,6 +80,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\AppFramework\\Http\\Attribute\\AuthorizedAdminSetting' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/AuthorizedAdminSetting.php',
'OCP\\AppFramework\\Http\\Attribute\\BruteForceProtection' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/BruteForceProtection.php',
'OCP\\AppFramework\\Http\\Attribute\\CORS' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/CORS.php',
+ 'OCP\\AppFramework\\Http\\Attribute\\ExAppRequired' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/ExAppRequired.php',
'OCP\\AppFramework\\Http\\Attribute\\FrontpageRoute' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/FrontpageRoute.php',
'OCP\\AppFramework\\Http\\Attribute\\IgnoreOpenAPI' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/IgnoreOpenAPI.php',
'OCP\\AppFramework\\Http\\Attribute\\NoAdminRequired' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/NoAdminRequired.php',
@@ -920,6 +921,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\AppFramework\\Middleware\\Security\\CSPMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/CSPMiddleware.php',
'OC\\AppFramework\\Middleware\\Security\\Exceptions\\AppNotEnabledException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php',
'OC\\AppFramework\\Middleware\\Security\\Exceptions\\CrossSiteRequestForgeryException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/CrossSiteRequestForgeryException.php',
+ 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\ExAppRequiredException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/ExAppRequiredException.php',
'OC\\AppFramework\\Middleware\\Security\\Exceptions\\LaxSameSiteCookieFailedException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/LaxSameSiteCookieFailedException.php',
'OC\\AppFramework\\Middleware\\Security\\Exceptions\\NotAdminException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/NotAdminException.php',
'OC\\AppFramework\\Middleware\\Security\\Exceptions\\NotConfirmedException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/NotConfirmedException.php',
diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/ExAppRequiredException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/ExAppRequiredException.php
new file mode 100644
index 00000000000..77bc7efebac
--- /dev/null
+++ b/lib/private/AppFramework/Middleware/Security/Exceptions/ExAppRequiredException.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\AppFramework\Middleware\Security\Exceptions;
+
+use OCP\AppFramework\Http;
+
+/**
+ * Class ExAppRequiredException is thrown when an endpoint can only be called by an ExApp but the caller is not an ExApp.
+ */
+class ExAppRequiredException extends SecurityException {
+ public function __construct() {
+ parent::__construct('ExApp required', Http::STATUS_PRECONDITION_FAILED);
+ }
+}
diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
index a38ad610fc6..bc2014da246 100644
--- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
+++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
@@ -10,16 +10,19 @@ namespace OC\AppFramework\Middleware\Security;
use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException;
use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException;
+use OC\AppFramework\Middleware\Security\Exceptions\ExAppRequiredException;
use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\Settings\AuthorizedGroupMapper;
+use OC\User\Session;
use OCP\App\AppPathNotFoundException;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
+use OCP\AppFramework\Http\Attribute\ExAppRequired;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
@@ -127,7 +130,12 @@ class SecurityMiddleware extends Middleware {
// security checks
$isPublicPage = $this->hasAnnotationOrAttribute($reflectionMethod, 'PublicPage', PublicPage::class);
- if (!$isPublicPage) {
+
+ if ($this->hasAnnotationOrAttribute($reflectionMethod, 'ExAppRequired', ExAppRequired::class)) {
+ if (!$this->userSession instanceof Session || $this->userSession->getSession()->get('app_api') !== true) {
+ throw new ExAppRequiredException();
+ }
+ } elseif (!$isPublicPage) {
if (!$this->isLoggedIn) {
throw new NotLoggedInException();
}
diff --git a/lib/public/AppFramework/Http/Attribute/ExAppRequired.php b/lib/public/AppFramework/Http/Attribute/ExAppRequired.php
new file mode 100644
index 00000000000..eb18da8027c
--- /dev/null
+++ b/lib/public/AppFramework/Http/Attribute/ExAppRequired.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCP\AppFramework\Http\Attribute;
+
+use Attribute;
+
+/**
+ * Attribute for controller methods that can only be accessed by ExApps
+ *
+ * @since 30.0.0
+ */
+#[Attribute]
+class ExAppRequired {
+}
diff --git a/tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php b/tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php
index dae26b2fad5..7d40d587c8e 100644
--- a/tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php
+++ b/tests/lib/AppFramework/Middleware/Security/Mock/SecurityMiddlewareController.php
@@ -9,6 +9,7 @@ declare(strict_types=1);
namespace Test\AppFramework\Middleware\Security\Mock;
+use OCP\AppFramework\Http\Attribute\ExAppRequired;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
@@ -156,4 +157,14 @@ class SecurityMiddlewareController extends \OCP\AppFramework\Controller {
#[PublicPage]
public function testAttributeNoAdminRequiredNoCSRFRequiredPublicPage() {
}
+
+ /**
+ * @ExAppRequired
+ */
+ public function testAnnotationExAppRequired() {
+ }
+
+ #[ExAppRequired]
+ public function testAttributeExAppRequired() {
+ }
}
diff --git a/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php
index 1d7753a3477..bda71c4e8ed 100644
--- a/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php
+++ b/tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php
@@ -11,6 +11,7 @@ use OC\AppFramework\Http;
use OC\AppFramework\Http\Request;
use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException;
use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException;
+use OC\AppFramework\Middleware\Security\Exceptions\ExAppRequiredException;
use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
@@ -18,6 +19,7 @@ use OC\Appframework\Middleware\Security\Exceptions\StrictCookieMissingException;
use OC\AppFramework\Middleware\Security\SecurityMiddleware;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\Settings\AuthorizedGroupMapper;
+use OC\User\Session;
use OCP\App\IAppManager;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\RedirectResponse;
@@ -27,6 +29,7 @@ use OCP\IL10N;
use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IRequestId;
+use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
@@ -66,7 +69,7 @@ class SecurityMiddlewareTest extends \Test\TestCase {
parent::setUp();
$this->authorizedGroupMapper = $this->createMock(AuthorizedGroupMapper::class);
- $this->userSession = $this->createMock(IUserSession::class);
+ $this->userSession = $this->createMock(Session::class);
$this->request = $this->createMock(IRequest::class);
$this->controller = new SecurityMiddlewareController(
'test',
@@ -167,6 +170,13 @@ class SecurityMiddlewareTest extends \Test\TestCase {
];
}
+ public static function dataExAppRequired(): array {
+ return [
+ ['testAnnotationExAppRequired'],
+ ['testAttributeExAppRequired'],
+ ];
+ }
+
/**
* @dataProvider dataNoCSRFRequiredPublicPage
*/
@@ -682,4 +692,40 @@ class SecurityMiddlewareTest extends \Test\TestCase {
$this->assertTrue($response instanceof JSONResponse);
}
+
+ /**
+ * @dataProvider dataExAppRequired
+ */
+ public function testExAppRequired(string $method): void {
+ $middleware = $this->getMiddleware(true, false, false);
+ $this->reader->reflect($this->controller, $method);
+
+ $session = $this->createMock(ISession::class);
+ $session->method('get')->with('app_api')->willReturn(true);
+ $this->userSession->method('getSession')->willReturn($session);
+
+ $this->request->expects($this->once())
+ ->method('passesStrictCookieCheck')
+ ->willReturn(true);
+ $this->request->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(true);
+
+ $middleware->beforeController($this->controller, $method);
+ }
+
+ /**
+ * @dataProvider dataExAppRequired
+ */
+ public function testExAppRequiredError(string $method): void {
+ $middleware = $this->getMiddleware(true, false, false, false);
+ $this->reader->reflect($this->controller, $method);
+
+ $session = $this->createMock(ISession::class);
+ $session->method('get')->with('app_api')->willReturn(false);
+ $this->userSession->method('getSession')->willReturn($session);
+
+ $this->expectException(ExAppRequiredException::class);
+ $middleware->beforeController($this->controller, $method);
+ }
}