From d04edfaf0dee3c2f1b4347a4ed36a79477d4a3f9 Mon Sep 17 00:00:00 2001 From: Lukas Reschke Date: Mon, 29 Feb 2016 17:30:02 +0100 Subject: [PATCH] Hides nodes from listing that the user has no access to --- apps/dav/lib/connector/legacydavacl.php | 4 +- apps/dav/lib/connector/sabre/davaclplugin.php | 72 +++++++ apps/dav/lib/server.php | 3 +- build/integration/config/behat.yml | 4 + .../features/bootstrap/CalDavContext.php | 172 ++++++++++++++++ .../features/bootstrap/CardDavContext.php | 193 ++++++++++++++++++ build/integration/features/caldav.feature | 31 +++ build/integration/features/carddav.feature | 23 +++ 8 files changed, 499 insertions(+), 3 deletions(-) create mode 100644 apps/dav/lib/connector/sabre/davaclplugin.php create mode 100644 build/integration/features/bootstrap/CalDavContext.php create mode 100644 build/integration/features/bootstrap/CardDavContext.php create mode 100644 build/integration/features/caldav.feature create mode 100644 build/integration/features/carddav.feature diff --git a/apps/dav/lib/connector/legacydavacl.php b/apps/dav/lib/connector/legacydavacl.php index 149bd85e4b..5a65460646 100644 --- a/apps/dav/lib/connector/legacydavacl.php +++ b/apps/dav/lib/connector/legacydavacl.php @@ -21,10 +21,10 @@ namespace OCA\DAV\Connector; - +use OCA\DAV\Connector\Sabre\DavAclPlugin; use Sabre\HTTP\URLUtil; -class LegacyDAVACL extends \Sabre\DAVACL\Plugin { +class LegacyDAVACL extends DavAclPlugin { /** * Converts the v1 principal `principal/` to the new v2 diff --git a/apps/dav/lib/connector/sabre/davaclplugin.php b/apps/dav/lib/connector/sabre/davaclplugin.php new file mode 100644 index 0000000000..4a9dd66161 --- /dev/null +++ b/apps/dav/lib/connector/sabre/davaclplugin.php @@ -0,0 +1,72 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\DAV\Connector\Sabre; + +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\IFile; +use Sabre\DAV\INode; +use \Sabre\DAV\PropFind; +use \Sabre\DAV\PropPatch; +use Sabre\DAVACL\Exception\NeedPrivileges; +use \Sabre\HTTP\RequestInterface; +use \Sabre\HTTP\ResponseInterface; +use Sabre\HTTP\URLUtil; + +/** + * Class DavAclPlugin is a wrapper around \Sabre\DAVACL\Plugin that returns 404 + * responses in case the resource to a response has been forbidden instead of + * a 403. This is used to prevent enumeration of valid resources. + * + * @see https://github.com/owncloud/core/issues/22578 + * @package OCA\DAV\Connector\Sabre + */ +class DavAclPlugin extends \Sabre\DAVACL\Plugin { + public function __construct() { + $this->hideNodesFromListings = true; + } + + function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true) { + $access = parent::checkPrivileges($uri, $privileges, $recursion, false); + if($access === false) { + /** @var INode $node */ + $node = $this->server->tree->getNodeForPath($uri); + + switch(get_class($node)) { + case 'OCA\DAV\CardDAV\AddressBook': + $type = 'Addressbook'; + break; + default: + $type = 'Node'; + break; + } + throw new NotFound( + sprintf( + "%s with name '%s' could not be found", + $type, + $node->getName() + ) + ); + } + + return $access; + } +} diff --git a/apps/dav/lib/server.php b/apps/dav/lib/server.php index 55ae6c62d3..2aa720c9dc 100644 --- a/apps/dav/lib/server.php +++ b/apps/dav/lib/server.php @@ -26,6 +26,7 @@ use OCA\DAV\CalDAV\Schedule\IMipPlugin; use OCA\DAV\Connector\FedAuth; use OCA\DAV\Connector\Sabre\Auth; use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; +use OCA\DAV\Connector\Sabre\DavAclPlugin; use OCA\DAV\Connector\Sabre\FilesPlugin; use OCA\DAV\Files\CustomPropertiesBackend; use OCP\IRequest; @@ -72,7 +73,7 @@ class Server { $this->server->addPlugin(new \Sabre\DAV\Sync\Plugin()); // acl - $acl = new \Sabre\DAVACL\Plugin(); + $acl = new DavAclPlugin(); $acl->defaultUsernamePath = 'principals/users'; $this->server->addPlugin($acl); diff --git a/build/integration/config/behat.yml b/build/integration/config/behat.yml index a1f9d610c6..d0c4586d28 100644 --- a/build/integration/config/behat.yml +++ b/build/integration/config/behat.yml @@ -16,6 +16,10 @@ default: baseUrl: http://localhost:8080 - TagsContext: baseUrl: http://localhost:8080 + - CardDavContext: + baseUrl: http://localhost:8080 + - CalDavContext: + baseUrl: http://localhost:8080 federation: paths: - %paths.base%/../federation_features diff --git a/build/integration/features/bootstrap/CalDavContext.php b/build/integration/features/bootstrap/CalDavContext.php new file mode 100644 index 0000000000..88711f3aa7 --- /dev/null +++ b/build/integration/features/bootstrap/CalDavContext.php @@ -0,0 +1,172 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +require __DIR__ . '/../../vendor/autoload.php'; + +use GuzzleHttp\Client; +use GuzzleHttp\Message\ResponseInterface; + +class CalDavContext implements \Behat\Behat\Context\Context { + /** @var string */ + private $baseUrl; + /** @var Client */ + private $client; + /** @var ResponseInterface */ + private $response; + /** @var string */ + private $responseXml = ''; + + /** + * @param string $baseUrl + */ + public function __construct($baseUrl) { + $this->baseUrl = $baseUrl; + + // in case of ci deployment we take the server url from the environment + $testServerUrl = getenv('TEST_SERVER_URL'); + if ($testServerUrl !== false) { + $this->baseUrl = substr($testServerUrl, 0, -5); + } + } + + /** @BeforeScenario */ + public function tearUpScenario() { + $this->client = new Client(); + $this->responseXml = ''; + } + + /** @AfterScenario */ + public function afterScenario() { + $davUrl = $this->baseUrl. '/remote.php/dav/calendars/admin/MyCalendar'; + try { + $this->client->delete( + $davUrl, + [ + 'auth' => [ + 'admin', + 'admin', + ], + ] + ); + } catch (\GuzzleHttp\Exception\ClientException $e) {} + } + + /** + * @When :user requests calendar :calendar + */ + public function requestsCalendar($user, $calendar) { + $davUrl = $this->baseUrl . '/remote.php/dav/calendars/'.$calendar; + + $password = ($user === 'admin') ? 'admin' : '123456'; + try { + $this->response = $this->client->get( + $davUrl, + [ + 'auth' => [ + $user, + $password, + ] + ] + ); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + } + + /** + * @Then The CalDAV HTTP status code should be :code + */ + public function theCaldavHttpStatusCodeShouldBe($code) { + if((int)$code !== $this->response->getStatusCode()) { + throw new \Exception( + sprintf( + 'Expected %s got %s', + (int)$code, + $this->response->getStatusCode() + ) + ); + } + + $body = $this->response->getBody()->getContents(); + if($body && substr($body, 0, 1) === '<') { + $reader = new Sabre\Xml\Reader(); + $reader->xml($body); + $this->responseXml = $reader->parse(); + } + } + + /** + * @Then The exception is :message + */ + public function theExceptionIs($message) { + $result = $this->responseXml['value'][0]['value']; + + if($message !== $result) { + throw new \Exception( + sprintf( + 'Expected %s got %s', + $message, + $result + ) + ); + } + } + + /** + * @Then The error message is :message + */ + public function theErrorMessageIs($message) { + $result = $this->responseXml['value'][1]['value']; + + if($message !== $result) { + throw new \Exception( + sprintf( + 'Expected %s got %s', + $message, + $result + ) + ); + } + } + + /** + * @Given :user creates a calendar named :name + */ + public function createsACalendarNamed($user, $name) { + $davUrl = $this->baseUrl . '/remote.php/dav/calendars/'.$user.'/'.$name; + $password = ($user === 'admin') ? 'admin' : '123456'; + + $request = $this->client->createRequest( + 'MKCALENDAR', + $davUrl, + [ + 'body' => 'test1#21213D', + 'auth' => [ + $user, + $password, + ], + ] + ); + + $this->response = $this->client->send($request); + } + +} diff --git a/build/integration/features/bootstrap/CardDavContext.php b/build/integration/features/bootstrap/CardDavContext.php new file mode 100644 index 0000000000..251d76d083 --- /dev/null +++ b/build/integration/features/bootstrap/CardDavContext.php @@ -0,0 +1,193 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see + * + */ + +require __DIR__ . '/../../vendor/autoload.php'; + +use GuzzleHttp\Client; +use GuzzleHttp\Message\ResponseInterface; + +class CardDavContext implements \Behat\Behat\Context\Context { + /** @var string */ + private $baseUrl; + /** @var Client */ + private $client; + /** @var ResponseInterface */ + private $response; + /** @var string */ + private $responseXml = ''; + + /** + * @param string $baseUrl + */ + public function __construct($baseUrl) { + $this->baseUrl = $baseUrl; + + // in case of ci deployment we take the server url from the environment + $testServerUrl = getenv('TEST_SERVER_URL'); + if ($testServerUrl !== false) { + $this->baseUrl = substr($testServerUrl, 0, -5); + } + } + + /** @BeforeScenario */ + public function tearUpScenario() { + $this->client = new Client(); + $this->responseXml = ''; + } + + + /** @AfterScenario */ + public function afterScenario() { + $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/admin/MyAddressbook'; + try { + $this->client->delete( + $davUrl, + [ + 'auth' => [ + 'admin', + 'admin', + ], + ] + ); + } catch (\GuzzleHttp\Exception\ClientException $e) {} + } + + + /** + * @When :user requests addressbook :addressBook with statuscode :statusCode + */ + public function requestsAddressbookWithStatuscode($user, $addressBook, $statusCode) { + $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/'.$addressBook; + + $password = ($user === 'admin') ? 'admin' : '123456'; + try { + $this->response = $this->client->get( + $davUrl, + [ + 'auth' => [ + $user, + $password, + ], + ] + ); + } catch (\GuzzleHttp\Exception\ClientException $e) { + $this->response = $e->getResponse(); + } + + if((int)$statusCode !== $this->response->getStatusCode()) { + throw new \Exception( + sprintf( + 'Expected %s got %s', + (int)$statusCode, + $this->response->getStatusCode() + ) + ); + } + + $body = $this->response->getBody()->getContents(); + if(substr($body, 0, 1) === '<') { + $reader = new Sabre\Xml\Reader(); + $reader->xml($body); + $this->responseXml = $reader->parse(); + } + } + + /** + * @Given :user creates an addressbook named :addressBook with statuscode :statusCode + */ + public function createsAnAddressbookNamedWithStatuscode($user, $addressBook, $statusCode) { + $davUrl = $this->baseUrl . '/remote.php/dav/addressbooks/users/'.$user.'/'.$addressBook; + $password = ($user === 'admin') ? 'admin' : '123456'; + + $request = $this->client->createRequest( + 'MKCOL', + $davUrl, + [ + 'body' => ' + + + + , + ,'.$addressBook.' + + + ', + 'auth' => [ + $user, + $password, + ], + 'headers' => [ + 'Content-Type' => 'application/xml;charset=UTF-8', + ], + ] + ); + + $this->response = $this->client->send($request); + + if($this->response->getStatusCode() !== (int)$statusCode) { + throw new \Exception( + sprintf( + 'Expected %s got %s', + (int)$statusCode, + $this->response->getStatusCode() + ) + ); + } + } + + /** + * @When The CardDAV exception is :message + */ + public function theCarddavExceptionIs($message) { + $result = $this->responseXml['value'][0]['value']; + + if($message !== $result) { + throw new \Exception( + sprintf( + 'Expected %s got %s', + $message, + $result + ) + ); + } + } + + /** + * @When The CardDAV error message is :arg1 + */ + public function theCarddavErrorMessageIs($message) { + $result = $this->responseXml['value'][1]['value']; + + if($message !== $result) { + throw new \Exception( + sprintf( + 'Expected %s got %s', + $message, + $result + ) + ); + } + } + +} diff --git a/build/integration/features/caldav.feature b/build/integration/features/caldav.feature new file mode 100644 index 0000000000..948151485d --- /dev/null +++ b/build/integration/features/caldav.feature @@ -0,0 +1,31 @@ +Feature: caldav + Scenario: Accessing a not existing calendar of another user + Given user "user0" exists + When "admin" requests calendar "user0/MyCalendar" + Then The CalDAV HTTP status code should be "404" + And The exception is "Sabre\DAV\Exception\NotFound" + And The error message is "Node with name 'MyCalendar' could not be found" + + # Blocked by https://github.com/php/php-src/pull/1417 + #Scenario: Accessing a not shared calendar of another user + # Given user "user0" exists + # Given "admin" creates a calendar named "MyCalendar" + # Given The CalDAV HTTP status code should be "201" + # When "user0" requests calendar "admin/MyCalendar" + # Then The CalDAV HTTP status code should be "404" + # And The exception is "Sabre\DAV\Exception\NotFound" + # And The error message is "Node with name 'MyCalendar' could not be found" + + Scenario: Accessing a not existing calendar of myself + Given user "user0" exists + When "user0" requests calendar "admin/MyCalendar" + Then The CalDAV HTTP status code should be "404" + And The exception is "Sabre\DAV\Exception\NotFound" + And The error message is "Node with name 'MyCalendar' could not be found" + + # Blocked by https://github.com/php/php-src/pull/1417 + #Scenario: Creating a new calendar + # When "admin" creates a calendar named "MyCalendar" + # Then The CalDAV HTTP status code should be "201" + # And "admin" requests calendar "admin/MyCalendar" + # Then The CalDAV HTTP status code should be "200" diff --git a/build/integration/features/carddav.feature b/build/integration/features/carddav.feature new file mode 100644 index 0000000000..ee9d877085 --- /dev/null +++ b/build/integration/features/carddav.feature @@ -0,0 +1,23 @@ +Feature: carddav + Scenario: Accessing a not existing addressbook of another user + Given user "user0" exists + When "admin" requests addressbook "user0/MyAddressbook" with statuscode "404" + And The CardDAV exception is "Sabre\DAV\Exception\NotFound" + And The CardDAV error message is "Addressbook with name 'MyAddressbook' could not be found" + + Scenario: Accessing a not shared addressbook of another user + Given user "user0" exists + Given "admin" creates an addressbook named "MyAddressbook" with statuscode "201" + When "user0" requests addressbook "admin/MyAddressbook" with statuscode "404" + And The CardDAV exception is "Sabre\DAV\Exception\NotFound" + And The CardDAV error message is "Addressbook with name 'MyAddressbook' could not be found" + + Scenario: Accessing a not existing addressbook of myself + Given user "user0" exists + When "user0" requests addressbook "admin/MyAddressbook" with statuscode "404" + And The CardDAV exception is "Sabre\DAV\Exception\NotFound" + And The CardDAV error message is "Addressbook with name 'MyAddressbook' could not be found" + + Scenario: Creating a new addressbook + When "admin" creates an addressbook named "MyAddressbook" with statuscode "201" + Then "admin" requests addressbook "admin/MyAddressbook" with statuscode "200"