From 8d9769ace82a2bd9a9295b006fe83ddf8a86649b 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 --- .../DependencyInjection/DIContainer.php | 2 +- ...are.php => PermissionPolicyMiddleware.php} | 36 ++-- .../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 | 26 +++ .../AppFramework/Http/TemplateResponse.php | 1 + .../FeaturePolicy/AddFeaturePolicyEvent.php | 3 + .../AddPermissionsPolicyEvent.php | 56 ++++++ ...php => PermissionPolicyMiddlewareTest.php} | 48 ++++- 13 files changed, 562 insertions(+), 21 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} (57%) diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 9a9740b7bccc4..a6772fd8bfc2c 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -268,7 +268,7 @@ public function __construct(string $appName, array $urlParams = [], ServerContai ) ); $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 418d4185184e5..0c958cf9a9f67 100644 --- a/lib/private/AppFramework/Middleware/Security/FeaturePolicyMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/PermissionPolicyMiddleware.php @@ -23,21 +23,30 @@ * along with this program. If not, see . * */ + 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; + + /** @var PermissionPolicyManager */ + private $permissionPolicyManager; - public function __construct(FeaturePolicyManager $policyManager) { - $this->policyManager = $policyManager; + public function __construct(FeaturePolicyManager $featurePolicyManager, PermissionPolicyManager $permissionPolicyManager) { + $this->featurePolicyManager = $featurePolicyManager; + $this->permissionPolicyManager = $permissionPolicyManager; } /** @@ -50,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..1aedeb6b3e255 --- /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 b73eaf667e73f..2f74f9a3c62de 100644 --- a/lib/public/AppFramework/Http/EmptyFeaturePolicy.php +++ b/lib/public/AppFramework/Http/EmptyFeaturePolicy.php @@ -34,6 +34,7 @@ * * @see \OCP\AppFramework\Http\FeaturePolicy * @since 17.0.0 + * @depreacted 21.0.0 Use \OCP\AppFramework\Http\EmptyPermissionPolicy */ class EmptyFeaturePolicy { /** @var string[] of allowed domains to autoplay media */ @@ -60,6 +61,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; @@ -72,6 +74,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; @@ -84,6 +87,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; @@ -96,6 +100,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; @@ -108,6 +113,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; @@ -120,6 +126,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; @@ -131,6 +138,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..1d94467e5a4f7 --- /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 d193dda546be0..39e4847ef1c43 100644 --- a/lib/public/AppFramework/Http/FeaturePolicy.php +++ b/lib/public/AppFramework/Http/FeaturePolicy.php @@ -35,6 +35,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..958080aef0df9 --- /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 152f8c4a3c5ea..593916883559e 100644 --- a/lib/public/AppFramework/Http/Response.php +++ b/lib/public/AppFramework/Http/Response.php @@ -85,6 +85,9 @@ class Response { /** @var FeaturePolicy */ private $featurePolicy; + /** @var PermissionPolicy */ + private $permissionPolicy; + /** @var bool */ private $throttled = false; /** @var array */ @@ -257,6 +260,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'] = 'noindex, nofollow'; if ($this->ETag) { @@ -316,6 +320,7 @@ public function getContentSecurityPolicy() { /** * @since 17.0.0 + * @depreacted 28.0.0 Use getPermissionPolicy */ public function getFeaturePolicy(): EmptyFeaturePolicy { if ($this->featurePolicy === null) { @@ -326,6 +331,7 @@ public function getFeaturePolicy(): EmptyFeaturePolicy { /** * @since 17.0.0 + * @depreacted 28.0.0 Use setPermissionPolicy */ public function setFeaturePolicy(EmptyFeaturePolicy $featurePolicy): self { $this->featurePolicy = $featurePolicy; @@ -333,6 +339,26 @@ public function setFeaturePolicy(EmptyFeaturePolicy $featurePolicy): self { return $this; } + /** + * @since 28.0.0 + */ + public function getPermissionPolicy(): EmptyPermissionPolicy { + if ($this->permissionPolicy === null) { + $this->setPermissionPolicy(new EmptyPermissionPolicy()); + } + return $this->permissionPolicy; + } + + /** + * @since 17.0.0 + * @depreacted 28.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 23843cd21d1cd..f4a14175ec001 100644 --- a/lib/public/AppFramework/Http/TemplateResponse.php +++ b/lib/public/AppFramework/Http/TemplateResponse.php @@ -111,6 +111,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 8d39b58a141c8..932a67984a9b9 100644 --- a/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php +++ b/lib/public/Security/FeaturePolicy/AddFeaturePolicyEvent.php @@ -35,6 +35,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 { /** @var FeaturePolicyManager */ @@ -42,6 +43,7 @@ class AddFeaturePolicyEvent extends Event { /** * @since 17.0.0 + * @depreacted 21.0.0 use AddPermissionPolicyEvent */ public function __construct(FeaturePolicyManager $policyManager) { parent::__construct(); @@ -50,6 +52,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..1a11387db3d97 --- /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 57% rename from tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php rename to tests/lib/AppFramework/Middleware/Security/PermissionPolicyMiddlewareTest.php index 154b43f69a5ba..65cc0c31eb5bd 100644 --- a/tests/lib/AppFramework/Middleware/Security/FeaturePolicyMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/PermissionPolicyMiddlewareTest.php @@ -25,16 +25,20 @@ 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 { - /** @var FeaturePolicyMiddleware|MockObject */ +class PermissionPolicyMiddlewareTest extends \Test\TestCase { + + /** @var PermissionPolicyMiddleware|MockObject */ private $middleware; /** @var Controller|MockObject */ private $controller; @@ -45,9 +49,11 @@ 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 ); } @@ -61,25 +67,49 @@ public function testAfterController() { $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); }