From f65ce546c05b054f7f3ac8c91e6040cd8dbd6e63 Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Sun, 1 Nov 2020 11:57:07 +0100 Subject: [PATCH] FeaturePolicy => PermissionPolicy We already had the FeaturePolicy header. However this call got renamed to PermisionPolicy. Here we move this over. The old mechanism stays there it just won't get extended. So apps that use the FeaturePolicy will not stop to work. Signed-off-by: Roeland Jago Douma --- lib/composer/composer/autoload_classmap.php | 7 +- lib/composer/composer/autoload_static.php | 7 +- .../DependencyInjection/DIContainer.php | 2 +- ...are.php => PermissionPolicyMiddleware.php} | 34 ++-- .../PermissionPolicy/PermissionPolicy.php | 76 ++++++++ .../PermissionPolicyManager.php | 94 ++++++++++ .../AppFramework/Http/EmptyFeaturePolicy.php | 8 + .../Http/EmptyPermissionPolicy.php | 173 ++++++++++++++++++ .../AppFramework/Http/FeaturePolicy.php | 1 + .../AppFramework/Http/PermissionPolicy.php | 59 ++++++ lib/public/AppFramework/Http/Response.php | 27 ++- .../AppFramework/Http/TemplateResponse.php | 1 + .../FeaturePolicy/AddFeaturePolicyEvent.php | 3 + .../AddPermissionsPolicyEvent.php | 56 ++++++ ...php => PermissionPolicyMiddlewareTest.php} | 51 +++++- 15 files changed, 574 insertions(+), 25 deletions(-) rename lib/private/AppFramework/Middleware/Security/{FeaturePolicyMiddleware.php => PermissionPolicyMiddleware.php} (51%) create mode 100644 lib/private/Security/PermissionPolicy/PermissionPolicy.php create mode 100644 lib/private/Security/PermissionPolicy/PermissionPolicyManager.php create mode 100644 lib/public/AppFramework/Http/EmptyPermissionPolicy.php create mode 100644 lib/public/AppFramework/Http/PermissionPolicy.php create mode 100644 lib/public/Security/PermissionsPolicy/AddPermissionsPolicyEvent.php rename tests/lib/AppFramework/Middleware/Security/{FeaturePolicyMiddlewareTest.php => PermissionPolicyMiddlewareTest.php} (56%) diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index b874bd58db..5d54414c1c 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -42,6 +42,7 @@ return array( 'OCP\\AppFramework\\Http\\DownloadResponse' => $baseDir . '/lib/public/AppFramework/Http/DownloadResponse.php', 'OCP\\AppFramework\\Http\\EmptyContentSecurityPolicy' => $baseDir . '/lib/public/AppFramework/Http/EmptyContentSecurityPolicy.php', 'OCP\\AppFramework\\Http\\EmptyFeaturePolicy' => $baseDir . '/lib/public/AppFramework/Http/EmptyFeaturePolicy.php', + 'OCP\\AppFramework\\Http\\EmptyPermissionPolicy' => $baseDir . '/lib/public/AppFramework/Http/EmptyPermissionPolicy.php', 'OCP\\AppFramework\\Http\\Events\\BeforeTemplateRenderedEvent' => $baseDir . '/lib/public/AppFramework/Http/Events/BeforeTemplateRenderedEvent.php', 'OCP\\AppFramework\\Http\\FeaturePolicy' => $baseDir . '/lib/public/AppFramework/Http/FeaturePolicy.php', 'OCP\\AppFramework\\Http\\FileDisplayResponse' => $baseDir . '/lib/public/AppFramework/Http/FileDisplayResponse.php', @@ -49,6 +50,7 @@ return array( 'OCP\\AppFramework\\Http\\IOutput' => $baseDir . '/lib/public/AppFramework/Http/IOutput.php', 'OCP\\AppFramework\\Http\\JSONResponse' => $baseDir . '/lib/public/AppFramework/Http/JSONResponse.php', 'OCP\\AppFramework\\Http\\NotFoundResponse' => $baseDir . '/lib/public/AppFramework/Http/NotFoundResponse.php', + 'OCP\\AppFramework\\Http\\PermissionPolicy' => $baseDir . '/lib/public/AppFramework/Http/PermissionPolicy.php', 'OCP\\AppFramework\\Http\\RedirectResponse' => $baseDir . '/lib/public/AppFramework/Http/RedirectResponse.php', 'OCP\\AppFramework\\Http\\RedirectToDefaultAppResponse' => $baseDir . '/lib/public/AppFramework/Http/RedirectToDefaultAppResponse.php', 'OCP\\AppFramework\\Http\\Response' => $baseDir . '/lib/public/AppFramework/Http/Response.php', @@ -461,6 +463,7 @@ return array( 'OCP\\Security\\ICrypto' => $baseDir . '/lib/public/Security/ICrypto.php', 'OCP\\Security\\IHasher' => $baseDir . '/lib/public/Security/IHasher.php', 'OCP\\Security\\ISecureRandom' => $baseDir . '/lib/public/Security/ISecureRandom.php', + 'OCP\\Security\\PermissionPolicy\\AddPermissionsPolicyEvent' => $baseDir . '/lib/public/Security/PermissionsPolicy/AddPermissionsPolicyEvent.php', 'OCP\\Session\\Exceptions\\SessionNotAvailableException' => $baseDir . '/lib/public/Session/Exceptions/SessionNotAvailableException.php', 'OCP\\Settings\\IIconSection' => $baseDir . '/lib/public/Settings/IIconSection.php', 'OCP\\Settings\\IManager' => $baseDir . '/lib/public/Settings/IManager.php', @@ -595,8 +598,8 @@ return array( 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\ReloadExecutionException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/ReloadExecutionException.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\SecurityException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\StrictCookieMissingException' => $baseDir . '/lib/private/AppFramework/Middleware/Security/Exceptions/StrictCookieMissingException.php', - 'OC\\AppFramework\\Middleware\\Security\\FeaturePolicyMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/FeaturePolicyMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\PasswordConfirmationMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php', + 'OC\\AppFramework\\Middleware\\Security\\PermissionPolicyMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/PermissionPolicyMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\RateLimitingMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\ReloadExecutionMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/ReloadExecutionMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\SameSiteCookieMiddleware' => $baseDir . '/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php', @@ -1303,6 +1306,8 @@ return array( 'OC\\Security\\IdentityProof\\Manager' => $baseDir . '/lib/private/Security/IdentityProof/Manager.php', 'OC\\Security\\IdentityProof\\Signer' => $baseDir . '/lib/private/Security/IdentityProof/Signer.php', 'OC\\Security\\Normalizer\\IpAddress' => $baseDir . '/lib/private/Security/Normalizer/IpAddress.php', + 'OC\\Security\\PermissionPolicy\\PermissionPolicy' => $baseDir . '/lib/private/Security/PermissionPolicy/PermissionPolicy.php', + 'OC\\Security\\PermissionPolicy\\PermissionPolicyManager' => $baseDir . '/lib/private/Security/PermissionPolicy/PermissionPolicyManager.php', 'OC\\Security\\RateLimiting\\Backend\\IBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/IBackend.php', 'OC\\Security\\RateLimiting\\Backend\\MemoryCache' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCache.php', 'OC\\Security\\RateLimiting\\Exception\\RateLimitExceededException' => $baseDir . '/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 26a7afff15..4e3a2444a9 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -71,6 +71,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\AppFramework\\Http\\DownloadResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/DownloadResponse.php', 'OCP\\AppFramework\\Http\\EmptyContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/EmptyContentSecurityPolicy.php', 'OCP\\AppFramework\\Http\\EmptyFeaturePolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/EmptyFeaturePolicy.php', + 'OCP\\AppFramework\\Http\\EmptyPermissionPolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/EmptyPermissionPolicy.php', 'OCP\\AppFramework\\Http\\Events\\BeforeTemplateRenderedEvent' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Events/BeforeTemplateRenderedEvent.php', 'OCP\\AppFramework\\Http\\FeaturePolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/FeaturePolicy.php', 'OCP\\AppFramework\\Http\\FileDisplayResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/FileDisplayResponse.php', @@ -78,6 +79,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\AppFramework\\Http\\IOutput' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/IOutput.php', 'OCP\\AppFramework\\Http\\JSONResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/JSONResponse.php', 'OCP\\AppFramework\\Http\\NotFoundResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/NotFoundResponse.php', + 'OCP\\AppFramework\\Http\\PermissionPolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/PermissionPolicy.php', 'OCP\\AppFramework\\Http\\RedirectResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/RedirectResponse.php', 'OCP\\AppFramework\\Http\\RedirectToDefaultAppResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/RedirectToDefaultAppResponse.php', 'OCP\\AppFramework\\Http\\Response' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Response.php', @@ -490,6 +492,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Security\\ICrypto' => __DIR__ . '/../../..' . '/lib/public/Security/ICrypto.php', 'OCP\\Security\\IHasher' => __DIR__ . '/../../..' . '/lib/public/Security/IHasher.php', 'OCP\\Security\\ISecureRandom' => __DIR__ . '/../../..' . '/lib/public/Security/ISecureRandom.php', + 'OCP\\Security\\PermissionPolicy\\AddPermissionsPolicyEvent' => __DIR__ . '/../../..' . '/lib/public/Security/PermissionsPolicy/AddPermissionsPolicyEvent.php', 'OCP\\Session\\Exceptions\\SessionNotAvailableException' => __DIR__ . '/../../..' . '/lib/public/Session/Exceptions/SessionNotAvailableException.php', 'OCP\\Settings\\IIconSection' => __DIR__ . '/../../..' . '/lib/public/Settings/IIconSection.php', 'OCP\\Settings\\IManager' => __DIR__ . '/../../..' . '/lib/public/Settings/IManager.php', @@ -624,8 +627,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\ReloadExecutionException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/ReloadExecutionException.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\SecurityException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php', 'OC\\AppFramework\\Middleware\\Security\\Exceptions\\StrictCookieMissingException' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/Exceptions/StrictCookieMissingException.php', - 'OC\\AppFramework\\Middleware\\Security\\FeaturePolicyMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/FeaturePolicyMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\PasswordConfirmationMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php', + 'OC\\AppFramework\\Middleware\\Security\\PermissionPolicyMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/PermissionPolicyMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\RateLimitingMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\ReloadExecutionMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/ReloadExecutionMiddleware.php', 'OC\\AppFramework\\Middleware\\Security\\SameSiteCookieMiddleware' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Middleware/Security/SameSiteCookieMiddleware.php', @@ -1332,6 +1335,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Security\\IdentityProof\\Manager' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Manager.php', 'OC\\Security\\IdentityProof\\Signer' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Signer.php', 'OC\\Security\\Normalizer\\IpAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Normalizer/IpAddress.php', + 'OC\\Security\\PermissionPolicy\\PermissionPolicy' => __DIR__ . '/../../..' . '/lib/private/Security/PermissionPolicy/PermissionPolicy.php', + 'OC\\Security\\PermissionPolicy\\PermissionPolicyManager' => __DIR__ . '/../../..' . '/lib/private/Security/PermissionPolicy/PermissionPolicyManager.php', 'OC\\Security\\RateLimiting\\Backend\\IBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/IBackend.php', 'OC\\Security\\RateLimiting\\Backend\\MemoryCache' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCache.php', 'OC\\Security\\RateLimiting\\Exception\\RateLimitExceededException' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php', diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 3ef816f503..e8ebd40045 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -256,7 +256,7 @@ class DIContainer extends SimpleContainer implements IAppContainer { ) ); $dispatcher->registerMiddleware( - $server->query(OC\AppFramework\Middleware\Security\FeaturePolicyMiddleware::class) + $server->query(OC\AppFramework\Middleware\Security\PermissionPolicyMiddleware::class) ); $dispatcher->registerMiddleware( new OC\AppFramework\Middleware\Security\PasswordConfirmationMiddleware( diff --git a/lib/private/AppFramework/Middleware/Security/FeaturePolicyMiddleware.php b/lib/private/AppFramework/Middleware/Security/PermissionPolicyMiddleware.php similarity index 51% rename from lib/private/AppFramework/Middleware/Security/FeaturePolicyMiddleware.php rename to lib/private/AppFramework/Middleware/Security/PermissionPolicyMiddleware.php index 63f665f512..fab60c037d 100644 --- a/lib/private/AppFramework/Middleware/Security/FeaturePolicyMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/PermissionPolicyMiddleware.php @@ -28,18 +28,25 @@ namespace OC\AppFramework\Middleware\Security; use OC\Security\FeaturePolicy\FeaturePolicy; use OC\Security\FeaturePolicy\FeaturePolicyManager; +use OC\Security\PermissionPolicy\PermissionPolicy; +use OC\Security\PermissionPolicy\PermissionPolicyManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\EmptyFeaturePolicy; +use OCP\AppFramework\Http\EmptyPermissionPolicy; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Middleware; -class FeaturePolicyMiddleware extends Middleware { +class PermissionPolicyMiddleware extends Middleware { /** @var FeaturePolicyManager */ - private $policyManager; + private $featurePolicyManager; - public function __construct(FeaturePolicyManager $policyManager) { - $this->policyManager = $policyManager; + /** @var PermissionPolicyManager */ + private $permissionPolicyManager; + + public function __construct(FeaturePolicyManager $featurePolicyManager, PermissionPolicyManager $permissionPolicyManager) { + $this->featurePolicyManager = $featurePolicyManager; + $this->permissionPolicyManager = $permissionPolicyManager; } /** @@ -52,15 +59,20 @@ class FeaturePolicyMiddleware extends Middleware { * @return Response */ public function afterController($controller, $methodName, Response $response): Response { - $policy = !is_null($response->getFeaturePolicy()) ? $response->getFeaturePolicy() : new FeaturePolicy(); - - if (get_class($policy) === EmptyFeaturePolicy::class) { - return $response; + $featurePolicy = !is_null($response->getFeaturePolicy()) ? $response->getFeaturePolicy() : new FeaturePolicy(); + if (get_class($featurePolicy) !== EmptyFeaturePolicy::class) { + $defaultPolicy = $this->featurePolicyManager->getDefaultPolicy(); + $defaultPolicy = $this->featurePolicyManager->mergePolicies($defaultPolicy, $featurePolicy); + $response->setFeaturePolicy($defaultPolicy); } - $defaultPolicy = $this->policyManager->getDefaultPolicy(); - $defaultPolicy = $this->policyManager->mergePolicies($defaultPolicy, $policy); - $response->setFeaturePolicy($defaultPolicy); + $permissionPolicy = !is_null($response->getPermissionPolicy()) ? $response->getPermissionPolicy() : new PermissionPolicy(); + if (get_class($permissionPolicy) !== EmptyPermissionPolicy::class) { + $defaultPolicy = $this->permissionPolicyManager->getDefaultPolicy(); + $defaultPolicy = $this->permissionPolicyManager->mergePolicies($defaultPolicy, $permissionPolicy); + $defaultPolicy = $this->permissionPolicyManager->mergeFeaturePolicy($defaultPolicy, $response->getFeaturePolicy()); + $response->setPermissionPolicy($defaultPolicy); + } return $response; } diff --git a/lib/private/Security/PermissionPolicy/PermissionPolicy.php b/lib/private/Security/PermissionPolicy/PermissionPolicy.php new file mode 100644 index 0000000000..87f4240f9a --- /dev/null +++ b/lib/private/Security/PermissionPolicy/PermissionPolicy.php @@ -0,0 +1,76 @@ + + * + * @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 . + * + */ + +namespace OC\Security\PermissionPolicy; + +class PermissionPolicy extends \OCP\AppFramework\Http\PermissionPolicy { + public function getAutoplayDomains(): array { + return $this->autoplayDomains; + } + + public function setAutoplayDomains(array $autoplayDomains): void { + $this->autoplayDomains = $autoplayDomains; + } + + public function getCameraDomains(): array { + return $this->cameraDomains; + } + + public function setCameraDomains(array $cameraDomains): void { + $this->cameraDomains = $cameraDomains; + } + + public function getFullscreenDomains(): array { + return $this->fullscreenDomains; + } + + public function setFullscreenDomains(array $fullscreenDomains): void { + $this->fullscreenDomains = $fullscreenDomains; + } + + public function getGeolocationDomains(): array { + return $this->geolocationDomains; + } + + public function setGeolocationDomains(array $geolocationDomains): void { + $this->geolocationDomains = $geolocationDomains; + } + + public function getMicrophoneDomains(): array { + return $this->microphoneDomains; + } + + public function setMicrophoneDomains(array $microphoneDomains): void { + $this->microphoneDomains = $microphoneDomains; + } + + public function getPaymentDomains(): array { + return $this->paymentDomains; + } + + public function setPaymentDomains(array $paymentDomains): void { + $this->paymentDomains = $paymentDomains; + } +} diff --git a/lib/private/Security/PermissionPolicy/PermissionPolicyManager.php b/lib/private/Security/PermissionPolicy/PermissionPolicyManager.php new file mode 100644 index 0000000000..1aedeb6b3e --- /dev/null +++ b/lib/private/Security/PermissionPolicy/PermissionPolicyManager.php @@ -0,0 +1,94 @@ + + * + * @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 . + * + */ + + +namespace OC\Security\PermissionPolicy; + +use OCP\AppFramework\Http\EmptyFeaturePolicy; +use OCP\AppFramework\Http\EmptyPermissionPolicy; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Security\PermissionPolicy\AddPermissionsPolicyEvent; + +class PermissionPolicyManager { + /** @var EmptyPermissionPolicy[] */ + private $policies = []; + + /** @var IEventDispatcher */ + private $dispatcher; + + public function __construct(IEventDispatcher $dispatcher) { + $this->dispatcher = $dispatcher; + } + + public function addDefaultPolicy(EmptyPermissionPolicy $policy): void { + $this->policies[] = $policy; + } + + public function getDefaultPolicy(): PermissionPolicy { + $event = new AddPermissionsPolicyEvent($this); + $this->dispatcher->dispatchTyped($event); + + $defaultPolicy = new PermissionPolicy(); + foreach ($this->policies as $policy) { + $defaultPolicy = $this->mergePolicies($defaultPolicy, $policy); + } + return $defaultPolicy; + } + + /** + * Merges the first given policy with the second one + * + */ + public function mergePolicies(PermissionPolicy $defaultPolicy, + EmptyPermissionPolicy $originalPolicy): PermissionPolicy { + foreach ((object)(array)$originalPolicy as $name => $value) { + $setter = 'set' . ucfirst($name); + if (\is_array($value)) { + $getter = 'get' . ucfirst($name); + $currentValues = \is_array($defaultPolicy->$getter()) ? $defaultPolicy->$getter() : []; + $defaultPolicy->$setter(\array_values(\array_unique(\array_merge($currentValues, $value)))); + } elseif (\is_bool($value)) { + $defaultPolicy->$setter($value); + } + } + + return $defaultPolicy; + } + + public function mergeFeaturePolicy(PermissionPolicy $defaultPolicy, EmptyFeaturePolicy $featurePolicy): PermissionPolicy { + foreach ((object)(array)$featurePolicy as $name => $value) { + $setter = 'set' . ucfirst($name); + if (\is_array($value)) { + $getter = 'get' . ucfirst($name); + $currentValues = \is_array($defaultPolicy->$getter()) ? $defaultPolicy->$getter() : []; + $defaultPolicy->$setter(\array_values(\array_unique(\array_merge($currentValues, $value)))); + } elseif (\is_bool($value)) { + $defaultPolicy->$setter($value); + } + } + + return $defaultPolicy; + } +} diff --git a/lib/public/AppFramework/Http/EmptyFeaturePolicy.php b/lib/public/AppFramework/Http/EmptyFeaturePolicy.php index 2e33d9ff31..e1d2987dc9 100644 --- a/lib/public/AppFramework/Http/EmptyFeaturePolicy.php +++ b/lib/public/AppFramework/Http/EmptyFeaturePolicy.php @@ -35,6 +35,7 @@ namespace OCP\AppFramework\Http; * * @see \OCP\AppFramework\Http\FeaturePolicy * @since 17.0.0 + * @depreacted 21.0.0 Use \OCP\AppFramework\Http\EmptyPermissionPolicy */ class EmptyFeaturePolicy { @@ -62,6 +63,7 @@ class EmptyFeaturePolicy { * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. * @return $this * @since 17.0.0 + * @depreacted 21.0.0 Use \OCP\AppFramework\Http\EmptyPermissionPolicy */ public function addAllowedAutoplayDomain(string $domain): self { $this->autoplayDomains[] = $domain; @@ -74,6 +76,7 @@ class EmptyFeaturePolicy { * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. * @return $this * @since 17.0.0 + * @depreacted 21.0.0 Use \OCP\AppFramework\Http\EmptyPermissionPolicy */ public function addAllowedCameraDomain(string $domain): self { $this->cameraDomains[] = $domain; @@ -86,6 +89,7 @@ class EmptyFeaturePolicy { * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. * @return $this * @since 17.0.0 + * @depreacted 21.0.0 Use \OCP\AppFramework\Http\EmptyPermissionPolicy */ public function addAllowedFullScreenDomain(string $domain): self { $this->fullscreenDomains[] = $domain; @@ -98,6 +102,7 @@ class EmptyFeaturePolicy { * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. * @return $this * @since 17.0.0 + * @depreacted 21.0.0 Use \OCP\AppFramework\Http\EmptyPermissionPolicy */ public function addAllowedGeoLocationDomain(string $domain): self { $this->geolocationDomains[] = $domain; @@ -110,6 +115,7 @@ class EmptyFeaturePolicy { * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. * @return $this * @since 17.0.0 + * @depreacted 21.0.0 Use \OCP\AppFramework\Http\EmptyPermissionPolicy */ public function addAllowedMicrophoneDomain(string $domain): self { $this->microphoneDomains[] = $domain; @@ -122,6 +128,7 @@ class EmptyFeaturePolicy { * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. * @return $this * @since 17.0.0 + * @depreacted 21.0.0 Use \OCP\AppFramework\Http\EmptyPermissionPolicy */ public function addAllowedPaymentDomain(string $domain): self { $this->paymentDomains[] = $domain; @@ -133,6 +140,7 @@ class EmptyFeaturePolicy { * * @return string * @since 17.0.0 + * @depreacted 21.0.0 Use \OCP\AppFramework\Http\EmptyPermissionPolicy */ public function buildPolicy(): string { $policy = ''; diff --git a/lib/public/AppFramework/Http/EmptyPermissionPolicy.php b/lib/public/AppFramework/Http/EmptyPermissionPolicy.php new file mode 100644 index 0000000000..1d94467e5a --- /dev/null +++ b/lib/public/AppFramework/Http/EmptyPermissionPolicy.php @@ -0,0 +1,173 @@ + + * + * @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 . + * + */ + +namespace OCP\AppFramework\Http; + +/** + * Class EmptyPermissionsPolicy is a simple helper which allows applications + * to modify the PermissionPolicy sent by Nextcloud. Per default the policy + * is forbidding everything. + * + * As alternative with sane exemptions look at PermissionPolicy + * + * @see \OCP\AppFramework\Http\FeaturePolicy + * @since 21.0.0 + */ +class EmptyPermissionPolicy { + + /** @var string[] of allowed domains to autoplay media */ + protected $autoplayDomains = null; + + /** @var string[] of allowed domains that can access the camera */ + protected $cameraDomains = null; + + /** @var string[] of allowed domains that can use fullscreen */ + protected $fullscreenDomains = null; + + /** @var string[] of allowed domains that can use the geolocation of the device */ + protected $geolocationDomains = null; + + /** @var string[] of allowed domains that can use the microphone */ + protected $microphoneDomains = null; + + /** @var string[] of allowed domains that can use the payment API */ + protected $paymentDomains = null; + + /** + * Allows to use autoplay from a specific domain. Use * to allow from all domains. + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 21.0.0 + */ + public function addAllowedAutoplayDomain(string $domain): self { + $this->autoplayDomains[] = $domain; + return $this; + } + + /** + * Allows to use the camera on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 21.0.0 + */ + public function addAllowedCameraDomain(string $domain): self { + $this->cameraDomains[] = $domain; + return $this; + } + + /** + * Allows the full screen functionality to be used on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 21.0.0 + */ + public function addAllowedFullScreenDomain(string $domain): self { + $this->fullscreenDomains[] = $domain; + return $this; + } + + /** + * Allows to use the geolocation on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 21.0.0 + */ + public function addAllowedGeoLocationDomain(string $domain): self { + $this->geolocationDomains[] = $domain; + return $this; + } + + /** + * Allows to use the microphone on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 21.0.0 + */ + public function addAllowedMicrophoneDomain(string $domain): self { + $this->microphoneDomains[] = $domain; + return $this; + } + + /** + * Allows to use the payment API on a specific domain. Use * to allow from all domains + * + * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized. + * @return $this + * @since 21.0.0 + */ + public function addAllowedPaymentDomain(string $domain): self { + $this->paymentDomains[] = $domain; + return $this; + } + + /** + * Get the generated Feature-Policy as a string + * + * @return string + * @since 21.0.0 + */ + public function buildPolicy(): string { + $policy = ''; + + $policy .= 'autoplay=(' . implode(' ', $this->formatDomainList($this->autoplayDomains)) . ') '; + $policy .= 'camera=(' . implode(' ', $this->formatDomainList($this->cameraDomains)) . ') '; + $policy .= 'fullscreen=(' . implode(' ', $this->formatDomainList($this->fullscreenDomains)) . ') '; + $policy .= 'geolocation=(' . implode(' ', $this->formatDomainList($this->geolocationDomains)) . ') '; + $policy .= 'microphone=(' . implode(' ', $this->formatDomainList($this->microphoneDomains)) . ') '; + $policy .= 'payment=(' . implode(' ', $this->formatDomainList($this->paymentDomains)) . ') '; + + return rtrim($policy, ' '); + } + + private function formatDomainList(?array $domains): array { + if ($domains === null) { + return []; + } + + $result = []; + + foreach ($domains as $domain) { + if (!is_string($domain)) { + // Ignore wrong entries + continue; + } + + if ($domain === '\'self\'') { + $domain = 'self'; + } + + $result[] = $domain; + } + + $result = array_unique($result); + + return $result; + } +} diff --git a/lib/public/AppFramework/Http/FeaturePolicy.php b/lib/public/AppFramework/Http/FeaturePolicy.php index 36315620ff..f220d61c12 100644 --- a/lib/public/AppFramework/Http/FeaturePolicy.php +++ b/lib/public/AppFramework/Http/FeaturePolicy.php @@ -36,6 +36,7 @@ namespace OCP\AppFramework\Http; * should require no modification at all for most use-cases. * * @since 17.0.0 + * @depreacted 21.0.0 use \OCP\AppFramework\Http\PermissionPolicy */ class FeaturePolicy extends EmptyFeaturePolicy { protected $autoplayDomains = [ diff --git a/lib/public/AppFramework/Http/PermissionPolicy.php b/lib/public/AppFramework/Http/PermissionPolicy.php new file mode 100644 index 0000000000..958080aef0 --- /dev/null +++ b/lib/public/AppFramework/Http/PermissionPolicy.php @@ -0,0 +1,59 @@ + + * + * @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 . + * + */ + +namespace OCP\AppFramework\Http; + +/** + * Class PermissionPolicy is a simple helper which allows applications to + * modify the Permission-Policy sent by Nextcloud. Per default only autoplay is allowed + * from the same domain and full screen as well from the same domain. + * + * Even if a value gets modified above defaults will still get appended. Please + * notice that Nextcloud ships already with sensible defaults and those policies + * should require no modification at all for most use-cases. + * + * @since 21.0.0 + */ +class PermissionPolicy extends EmptyPermissionPolicy { + protected $autoplayDomains = [ + 'self', + ]; + + /** @var string[] of allowed domains that can access the camera */ + protected $cameraDomains = []; + + protected $fullscreenDomains = [ + 'self', + ]; + + /** @var string[] of allowed domains that can use the geolocation of the device */ + protected $geolocationDomains = []; + + /** @var string[] of allowed domains that can use the microphone */ + protected $microphoneDomains = []; + + /** @var string[] of allowed domains that can use the payment API */ + protected $paymentDomains = []; +} diff --git a/lib/public/AppFramework/Http/Response.php b/lib/public/AppFramework/Http/Response.php index ff6b97f87b..c6099419dc 100644 --- a/lib/public/AppFramework/Http/Response.php +++ b/lib/public/AppFramework/Http/Response.php @@ -89,6 +89,9 @@ class Response { /** @var FeaturePolicy */ private $featurePolicy; + /** @var PermissionPolicy */ + private $permissionPolicy; + /** @var bool */ private $throttled = false; /** @var array */ @@ -240,7 +243,7 @@ class Response { } $this->headers['Content-Security-Policy'] = $this->getContentSecurityPolicy()->buildPolicy(); - $this->headers['Feature-Policy'] = $this->getFeaturePolicy()->buildPolicy(); + $this->headers['Permissions-Policy'] = $this->getPermissionPolicy()->buildPolicy(); $this->headers['X-Robots-Tag'] = 'none'; if ($this->ETag) { @@ -300,6 +303,7 @@ class Response { /** * @since 17.0.0 + * @depreacted 21.0.0 Use getPermissionPolicy */ public function getFeaturePolicy(): EmptyFeaturePolicy { if ($this->featurePolicy === null) { @@ -310,6 +314,7 @@ class Response { /** * @since 17.0.0 + * @depreacted 21.0.0 Use setPermissionPolicy */ public function setFeaturePolicy(EmptyFeaturePolicy $featurePolicy): self { $this->featurePolicy = $featurePolicy; @@ -317,6 +322,26 @@ class Response { return $this; } + /** + * @since 21.0.0 + */ + public function getPermissionPolicy(): EmptyPermissionPolicy { + if ($this->permissionPolicy === null) { + $this->setPermissionPolicy(new EmptyPermissionPolicy()); + } + return $this->permissionPolicy; + } + + /** + * @since 17.0.0 + * @depreacted 21.0.0 Use setPermissionPolicy + */ + public function setPermissionPolicy(EmptyPermissionPolicy $permissionPolicy): self { + $this->permissionPolicy = $permissionPolicy; + + return $this; + } + /** diff --git a/lib/public/AppFramework/Http/TemplateResponse.php b/lib/public/AppFramework/Http/TemplateResponse.php index 837ada337b..264779e469 100644 --- a/lib/public/AppFramework/Http/TemplateResponse.php +++ b/lib/public/AppFramework/Http/TemplateResponse.php @@ -117,6 +117,7 @@ class TemplateResponse extends Response { $this->setContentSecurityPolicy(new ContentSecurityPolicy()); $this->setFeaturePolicy(new FeaturePolicy()); + $this->setPermissionPolicy(new PermissionPolicy()); } diff --git a/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php b/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php index 764b57c31e..fa066e4c10 100644 --- a/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php +++ b/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php @@ -36,6 +36,7 @@ use OCP\EventDispatcher\Event; * Event that allows to register a feature policy header to a request. * * @since 17.0.0 + * @depreacted 21.0.0 use AddPermissionPolicyEvent */ class AddFeaturePolicyEvent extends Event { @@ -44,6 +45,7 @@ class AddFeaturePolicyEvent extends Event { /** * @since 17.0.0 + * @depreacted 21.0.0 use AddPermissionPolicyEvent */ public function __construct(FeaturePolicyManager $policyManager) { parent::__construct(); @@ -52,6 +54,7 @@ class AddFeaturePolicyEvent extends Event { /** * @since 17.0.0 + * @depreacted 21.0.0 use AddPermissionPolicyEvent */ public function addPolicy(EmptyFeaturePolicy $policy) { $this->policyManager->addDefaultPolicy($policy); diff --git a/lib/public/Security/PermissionsPolicy/AddPermissionsPolicyEvent.php b/lib/public/Security/PermissionsPolicy/AddPermissionsPolicyEvent.php new file mode 100644 index 0000000000..1a11387db3 --- /dev/null +++ b/lib/public/Security/PermissionsPolicy/AddPermissionsPolicyEvent.php @@ -0,0 +1,56 @@ + + * + * @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 . + * + */ + +namespace OCP\Security\PermissionPolicy; + +use OC\Security\PermissionPolicy\PermissionPolicyManager; +use OCP\AppFramework\Http\EmptyPermissionPolicy; +use OCP\EventDispatcher\Event; + +/** + * Event that allows to register a feature policy header to a request. + * + * @since 21.0.0 + */ +class AddPermissionsPolicyEvent extends Event { + + /** @var PermissionPolicyManager */ + private $policyManager; + + /** + * @since 21.0.0 + */ + public function __construct(PermissionPolicyManager $policyManager) { + parent::__construct(); + $this->policyManager = $policyManager; + } + + /** + * @since 21.0.0 + */ + public function addPolicy(EmptyPermissionPolicy $policy) { + $this->policyManager->addDefaultPolicy($policy); + } +} diff --git a/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/PermissionPolicyMiddlewareTest.php similarity index 56% rename from tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php rename to tests/lib/AppFramework/Middleware/Security/PermissionPolicyMiddlewareTest.php index 0a4b3c4f34..7049aadf1c 100644 --- a/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/PermissionPolicyMiddlewareTest.php @@ -25,30 +25,37 @@ declare(strict_types=1); namespace Test\AppFramework\Middleware\Security; -use OC\AppFramework\Middleware\Security\FeaturePolicyMiddleware; +use OC\AppFramework\Middleware\Security\PermissionPolicyMiddleware; use OC\Security\FeaturePolicy\FeaturePolicy; use OC\Security\FeaturePolicy\FeaturePolicyManager; +use OC\Security\PermissionPolicy\PermissionPolicy; +use OC\Security\PermissionPolicy\PermissionPolicyManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\EmptyFeaturePolicy; +use OCP\AppFramework\Http\EmptyPermissionPolicy; use OCP\AppFramework\Http\Response; use PHPUnit\Framework\MockObject\MockObject; -class FeaturePolicyMiddlewareTest extends \Test\TestCase { +class PermissionPolicyMiddlewareTest extends \Test\TestCase { - /** @var FeaturePolicyMiddleware|MockObject */ + /** @var PermissionPolicyMiddleware|MockObject */ private $middleware; /** @var Controller|MockObject */ private $controller; /** @var FeaturePolicyManager|MockObject */ - private $manager; + private $featurePolicyManager; + /** @var PermissionPolicyManager|MockObject */ + private $permissionPolicyManager; protected function setUp(): void { parent::setUp(); $this->controller = $this->createMock(Controller::class); - $this->manager = $this->createMock(FeaturePolicyManager::class); - $this->middleware = new FeaturePolicyMiddleware( - $this->manager + $this->featurePolicyManager = $this->createMock(FeaturePolicyManager::class); + $this->permissionPolicyManager = $this->createMock(PermissionPolicyManager::class); + $this->middleware = new PermissionPolicyMiddleware( + $this->featurePolicyManager, + $this->permissionPolicyManager ); } @@ -62,25 +69,49 @@ class FeaturePolicyMiddlewareTest extends \Test\TestCase { $mergedPolicy->addAllowedGeoLocationDomain('mergedPolicy'); $response->method('getFeaturePolicy') ->willReturn($currentPolicy); - $this->manager->method('getDefaultPolicy') + $this->featurePolicyManager->method('getDefaultPolicy') ->willReturn($defaultPolicy); - $this->manager->method('mergePolicies') + $this->featurePolicyManager->method('mergePolicies') ->with($defaultPolicy, $currentPolicy) ->willReturn($mergedPolicy); $response->expects($this->once()) ->method('setFeaturePolicy') ->with($mergedPolicy); + $defaultPermissionPolicy = new PermissionPolicy(); + $this->permissionPolicyManager->method('getDefaultPolicy') + ->willReturn($defaultPermissionPolicy); + $currentPermissionPolicy = new PermissionPolicy(); + $response->method('getPermissionPolicy') + ->willReturn($currentPermissionPolicy); + $mergedPermissionPolicy = new PermissionPolicy(); + $this->permissionPolicyManager->method('mergePolicies') + ->with($defaultPermissionPolicy, $currentPermissionPolicy) + ->willReturn($mergedPermissionPolicy); + $mergedPermissionPolicyWithFeaturePolicy = new PermissionPolicy(); + $this->permissionPolicyManager->method('mergeFeaturePolicy') + ->with($mergedPermissionPolicy, $currentPolicy) + ->willReturn($mergedPermissionPolicyWithFeaturePolicy); + + $response->expects($this->once()) + ->method('setPermissionPolicy') + ->with($mergedPermissionPolicy); + $this->middleware->afterController($this->controller, 'test', $response); } - public function testAfterControllerEmptyCSP() { + public function testAfterControllerEmpty() { $response = $this->createMock(Response::class); $emptyPolicy = new EmptyFeaturePolicy(); + $emptyPermissionPolicy = new EmptyPermissionPolicy(); $response->method('getFeaturePolicy') ->willReturn($emptyPolicy); + $response->method('getPermissionPolicy') + ->willReturn($emptyPermissionPolicy); $response->expects($this->never()) ->method('setFeaturePolicy'); + $response->expects($this->never()) + ->method('setPermissionPolicy'); $this->middleware->afterController($this->controller, 'test', $response); }