From 3a935c72e0f91ab332879a4e0d5885ae7b66bae2 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 | 93 ++++++++++ .../AppFramework/Http/EmptyFeaturePolicy.php | 8 + .../Http/EmptyPermissionPolicy.php | 173 ++++++++++++++++++ .../AppFramework/Http/FeaturePolicy.php | 1 + .../AppFramework/Http/PermissionPolicy.php | 58 ++++++ lib/public/AppFramework/Http/Response.php | 27 ++- .../AppFramework/Http/TemplateResponse.php | 1 + .../FeaturePolicy/AddFeaturePolicyEvent.php | 3 + .../AddPermissionsPolicyEvent.php | 58 ++++++ .../Security/FeaturePolicyMiddlewareTest.php | 6 +- 15 files changed, 536 insertions(+), 18 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 diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index a966ad04f5ff4..7887f65290916 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -42,6 +42,7 @@ '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', @@ -50,6 +51,7 @@ '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\\OCSResponse' => $baseDir . '/lib/public/AppFramework/Http/OCSResponse.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', @@ -462,6 +464,7 @@ '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', @@ -596,8 +599,8 @@ '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', @@ -1295,6 +1298,8 @@ '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 92692d2188c88..d1462817bbb55 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', @@ -79,6 +80,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c '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\\OCSResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/OCSResponse.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', @@ -491,6 +493,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', @@ -625,8 +628,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', @@ -1324,6 +1327,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 3ef816f503e41..e8ebd4004524d 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -256,7 +256,7 @@ public function __construct($appName, $urlParams = [], ServerContainer $server = ) ); $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 63f665f512d11..fab60c037d077 100644 --- a/lib/private/AppFramework/Middleware/Security/FeaturePolicyMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/PermissionPolicyMiddleware.php @@ -28,18 +28,25 @@ 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 @@ public function __construct(FeaturePolicyManager $policyManager) { * @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 0000000000000..87f4240f9ad63 --- /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 0000000000000..14151e928f651 --- /dev/null +++ b/lib/private/Security/PermissionPolicy/PermissionPolicyManager.php @@ -0,0 +1,93 @@ + + * + * @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 2e33d9ff31bb6..e1d2987dc9824 100644 --- a/lib/public/AppFramework/Http/EmptyFeaturePolicy.php +++ b/lib/public/AppFramework/Http/EmptyFeaturePolicy.php @@ -35,6 +35,7 @@ * * @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 @@ public function addAllowedAutoplayDomain(string $domain): self { * @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 @@ public function addAllowedCameraDomain(string $domain): self { * @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 @@ public function addAllowedFullScreenDomain(string $domain): self { * @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 @@ public function addAllowedGeoLocationDomain(string $domain): self { * @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 @@ public function addAllowedMicrophoneDomain(string $domain): self { * @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 @@ public function addAllowedPaymentDomain(string $domain): self { * * @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 0000000000000..b18574a73f333 --- /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 36315620ffee5..f220d61c12b78 100644 --- a/lib/public/AppFramework/Http/FeaturePolicy.php +++ b/lib/public/AppFramework/Http/FeaturePolicy.php @@ -36,6 +36,7 @@ * 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 0000000000000..6a1d7630aa568 --- /dev/null +++ b/lib/public/AppFramework/Http/PermissionPolicy.php @@ -0,0 +1,58 @@ + + * + * @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 ff6b97f87b123..c6099419dc196 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 @@ public function getHeaders() { } $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 @@ public function getContentSecurityPolicy() { /** * @since 17.0.0 + * @depreacted 21.0.0 Use getPermissionPolicy */ public function getFeaturePolicy(): EmptyFeaturePolicy { if ($this->featurePolicy === null) { @@ -310,6 +314,7 @@ public function getFeaturePolicy(): EmptyFeaturePolicy { /** * @since 17.0.0 + * @depreacted 21.0.0 Use setPermissionPolicy */ public function setFeaturePolicy(EmptyFeaturePolicy $featurePolicy): self { $this->featurePolicy = $featurePolicy; @@ -317,6 +322,26 @@ public function setFeaturePolicy(EmptyFeaturePolicy $featurePolicy): self { 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 837ada337b88c..264779e469bfe 100644 --- a/lib/public/AppFramework/Http/TemplateResponse.php +++ b/lib/public/AppFramework/Http/TemplateResponse.php @@ -117,6 +117,7 @@ public function __construct($appName, $templateName, array $params = [], $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 764b57c31ea9e..fa066e4c105ed 100644 --- a/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php +++ b/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php @@ -36,6 +36,7 @@ * 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 @@ public function __construct(FeaturePolicyManager $policyManager) { /** * @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 0000000000000..5a4ad536e964f --- /dev/null +++ b/lib/public/Security/PermissionsPolicy/AddPermissionsPolicyEvent.php @@ -0,0 +1,58 @@ + + * + * @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\FeaturePolicy\FeaturePolicyManager; +use OC\Security\FeaturePolicy\PermissionPolicyManager; +use OCP\AppFramework\Http\EmptyFeaturePolicy; +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/FeaturePolicyMiddlewareTest.php index 0a4b3c4f34c24..283a8b477a8c6 100644 --- a/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php @@ -25,7 +25,7 @@ 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 OCP\AppFramework\Controller; @@ -35,7 +35,7 @@ class FeaturePolicyMiddlewareTest extends \Test\TestCase { - /** @var FeaturePolicyMiddleware|MockObject */ + /** @var PermissionPolicyMiddleware|MockObject */ private $middleware; /** @var Controller|MockObject */ private $controller; @@ -47,7 +47,7 @@ protected function setUp(): void { $this->controller = $this->createMock(Controller::class); $this->manager = $this->createMock(FeaturePolicyManager::class); - $this->middleware = new FeaturePolicyMiddleware( + $this->middleware = new PermissionPolicyMiddleware( $this->manager ); }