From 357d3a86d697ec74b56dc993b8f0ce0c9f80fab1 Mon Sep 17 00:00:00 2001 From: "v.scharf" Date: Sun, 18 Oct 2026 07:40:56 +0200 Subject: [PATCH] tests: check that a guest can only access the shared resource Make sure the guest session gives no access outside of the shared folder. --- .../acceptance/bootstrap/GuestLinkContext.php | 296 ++++++++++++++++-- .../apiGuestLink/guestLinkSecurity.feature | 108 +++++++ 2 files changed, 384 insertions(+), 20 deletions(-) create mode 100644 tests/acceptance/features/apiGuestLink/guestLinkSecurity.feature diff --git a/tests/acceptance/bootstrap/GuestLinkContext.php b/tests/acceptance/bootstrap/GuestLinkContext.php index 1819d22ac1..731b4085e3 100644 --- a/tests/acceptance/bootstrap/GuestLinkContext.php +++ b/tests/acceptance/bootstrap/GuestLinkContext.php @@ -29,6 +29,7 @@ use Psr\Http\Message\ResponseInterface; use TestHelpers\BehatHelper; use TestHelpers\EmailHelper; +use TestHelpers\GraphHelper; use TestHelpers\GuestLinkHelper; use TestHelpers\HttpRequestHelper; use TestHelpers\WaitHelper; @@ -156,9 +157,7 @@ private function readNewEmail(string $guest): string { "Guest '$guest' was expected to have received a new email" ); $this->readEmailCount[$guest] = \count($emails); - $body = EmailHelper::getBodyOfLastEmail($guest, $this->featureContext->getStepLineRef()); - $this->rememberCredentialsFromEmail($guest, $body); - return $body; + return $this->getBodyOfLastEmail($guest); } /** @@ -173,6 +172,16 @@ private function readLastEmail(string $guest): string { if (($this->readEmailCount[$guest] ?? 0) === 0) { return $this->readNewEmail($guest); } + return $this->getBodyOfLastEmail($guest); + } + + /** + * @param string $guest + * + * @return string + * @throws GuzzleException + */ + private function getBodyOfLastEmail(string $guest): string { $body = EmailHelper::getBodyOfLastEmail($guest, $this->featureContext->getStepLineRef()); $this->rememberCredentialsFromEmail($guest, $body); return $body; @@ -185,11 +194,20 @@ private function readLastEmail(string $guest): string { * @throws GuzzleException */ private function getGuestLinkTokenFromLastEmail(string $guest): string { - $body = $this->readLastEmail($guest); + return $this->getGuestLinkToken($guest, $this->readLastEmail($guest)); + } + + /** + * @param string $guest + * @param string $body + * + * @return string + */ + private function getGuestLinkToken(string $guest, string $body): string { Assert::assertMatchesRegularExpression( GuestLinkHelper::TOKEN_IN_EMAIL_REGEX, $body, - "The last email of guest '$guest' does not contain a guest link" + "The email of guest '$guest' does not contain a guest link" ); \preg_match(GuestLinkHelper::TOKEN_IN_EMAIL_REGEX, $body, $matches); return $matches[1]; @@ -285,12 +303,7 @@ public function guestHasVerifiedTheGuestLinkFromTheLastEmail(string $guest): voi */ #[Given('guest :guest has received a new guest link')] public function guestHasReceivedANewGuestLink(string $guest): void { - $body = $this->readNewEmail($guest); - Assert::assertMatchesRegularExpression( - GuestLinkHelper::TOKEN_IN_EMAIL_REGEX, - $body, - "The new email of guest '$guest' does not contain a guest link" - ); + $this->getGuestLinkToken($guest, $this->readNewEmail($guest)); } /** @@ -348,15 +361,25 @@ public function guestListsTheSharesSharedWithThem(string $guest): void { * @throws GuzzleException */ #[When('guest :guest downloads file :resource shared from space :space of user :owner using the WebDAV API')] - public function guestDownloadsSharedFile(string $guest, string $resource, string $space, string $owner): void { + public function guestDownloadsSharedFile( + string $guest, + string $resource, + string $space, + string $owner + ): void { $fileId = $this->spacesContext->getResourceId($owner, $space, $resource); $this->featureContext->setResponse( - HttpRequestHelper::get( - $this->featureContext->getBaseUrl() . "/" . WebDavHelper::prefixRemotePhp("dav/spaces/$fileId"), - $this->featureContext->getStepLineRef(), + WebDavHelper::makeDavRequest( + $this->featureContext->getBaseUrl(), null, null, - $this->getSessionHeaders($guest), + 'GET', + '', + \array_merge($this->getSessionHeaders($guest), []), + $fileId, + $this->featureContext->getStepLineRef(), + null, + WebDavHelper::DAV_VERSION_SPACES ) ); } @@ -381,18 +404,252 @@ public function guestUploadsContentToSharedFile( ): void { $fileId = $this->spacesContext->getResourceId($owner, $space, $resource); $this->featureContext->setResponse( - HttpRequestHelper::sendRequest( - $this->featureContext->getBaseUrl() . "/" . WebDavHelper::prefixRemotePhp("dav/spaces/$fileId"), + WebDavHelper::makeDavRequest( + $this->featureContext->getBaseUrl(), + null, + null, + 'PUT', + '', + \array_merge($this->getSessionHeaders($guest), []), + $fileId, + $this->featureContext->getStepLineRef(), + $content, + WebDavHelper::DAV_VERSION_SPACES + ) + ); + } + + /** + * @param string $guest + * @param string $folder + * @param string $space + * @param string $owner + * + * @return void + * @throws GuzzleException + */ + #[When('guest :guest lists the content of folder :folder of space :space of user :owner using the WebDAV API')] + public function guestListsTheContentOfFolder( + string $guest, + string $folder, + string $space, + string $owner + ): void { + $folderId = $this->spacesContext->getResourceId($owner, $space, $folder); + $this->featureContext->setResponse( + WebDavHelper::makeDavRequest( + $this->featureContext->getBaseUrl(), + null, + null, + 'PROPFIND', + '', + \array_merge($this->getSessionHeaders($guest), ['Depth' => '1']), + $folderId, $this->featureContext->getStepLineRef(), + null, + WebDavHelper::DAV_VERSION_SPACES + ) + ); + } + + /** + * @param string $guest + * @param string $content + * @param string $path + * @param string $folder + * @param string $space + * @param string $owner + * + * @return void + * @throws GuzzleException + */ + #[When('guest :guest uploads content :content to :path inside folder :folder of space :space of user :owner using the WebDAV API')] + public function guestUploadsContentInsideFolder( + string $guest, + string $content, + string $path, + string $folder, + string $space, + string $owner + ): void { + $folderId = $this->spacesContext->getResourceId($owner, $space, $folder); + $this->featureContext->setResponse( + WebDavHelper::makeDavRequest( + $this->featureContext->getBaseUrl(), + null, + null, 'PUT', + $path, + \array_merge($this->getSessionHeaders($guest), []), + $folderId, + $this->featureContext->getStepLineRef(), + $content, + WebDavHelper::DAV_VERSION_SPACES + ) + ); + } + + /** + * @param string $guest + * @param string $path + * @param string $folder + * @param string $space + * @param string $owner + * + * @return void + * @throws GuzzleException + */ + #[When('guest :guest creates folder :path inside folder :folder of space :space of user :owner using the WebDAV API')] + public function guestCreatesFolderInsideFolder( + string $guest, + string $path, + string $folder, + string $space, + string $owner + ): void { + $folderId = $this->spacesContext->getResourceId($owner, $space, $folder); + $this->featureContext->setResponse( + WebDavHelper::makeDavRequest( + $this->featureContext->getBaseUrl(), + null, + null, + 'MKCOL', + $path, + \array_merge($this->getSessionHeaders($guest), []), + $folderId, + $this->featureContext->getStepLineRef(), + null, + WebDavHelper::DAV_VERSION_SPACES + ) + ); + } + + /** + * @param string $guest + * @param string $path + * @param string $folder + * @param string $space + * @param string $owner + * @param string $newName + * + * @return void + * @throws GuzzleException + */ + #[When('guest :guest renames :path inside folder :folder of space :space of user :owner to :newName using the WebDAV API')] + public function guestRenamesInsideFolder( + string $guest, + string $path, + string $folder, + string $space, + string $owner, + string $newName + ): void { + $folderId = $this->spacesContext->getResourceId($owner, $space, $folder); + $this->featureContext->setResponse( + WebDavHelper::makeDavRequest( + $this->featureContext->getBaseUrl(), + null, + null, + 'MOVE', + $path, + \array_merge($this->getSessionHeaders($guest), ['Destination' => $this->featureContext->getBaseUrl() . '/' . WebDavHelper::getDavPath(WebDavHelper::DAV_VERSION_SPACES, $folderId) . '/' . $newName]), + $folderId, + $this->featureContext->getStepLineRef(), + null, + WebDavHelper::DAV_VERSION_SPACES + ) + ); + } + + /** + * @param string $guest + * @param string $path + * @param string $folder + * @param string $space + * @param string $owner + * @param string $destination + * + * @return void + * @throws GuzzleException + */ + #[When('guest :guest tries to move :path inside folder :folder of space :space of user :owner to :destination in the root of that space using the WebDAV API')] + public function guestTriesToMoveOutOfTheSharedFolder( + string $guest, + string $path, + string $folder, + string $space, + string $owner, + string $destination + ): void { + $folderId = $this->spacesContext->getResourceId($owner, $space, $folder); + $spaceRootId = $this->spacesContext->getResourceId($owner, $space, ''); + $this->featureContext->setResponse( + WebDavHelper::makeDavRequest( + $this->featureContext->getBaseUrl(), + null, + null, + 'MOVE', + $path, + \array_merge($this->getSessionHeaders($guest), ['Destination' => $this->featureContext->getBaseUrl() . '/' . WebDavHelper::getDavPath(WebDavHelper::DAV_VERSION_SPACES, $spaceRootId) . '/' . $destination]), + $folderId, + $this->featureContext->getStepLineRef(), + null, + WebDavHelper::DAV_VERSION_SPACES + ) + ); + } + + /** + * @param string $guest + * @param string $method + * @param string $endpoint e.g. "invite", "createLink" or "permissions" + * @param string $resource + * @param string $space + * @param string $owner + * + * @return void + * @throws GuzzleException + */ + #[When('guest :guest sends HTTP method :method to the Graph endpoint :endpoint of resource :resource of space :space of user :owner')] + public function guestSendsHttpMethodToGraphEndpointOfResource( + string $guest, + string $method, + string $endpoint, + string $resource, + string $space, + string $owner + ): void { + $spaceId = $this->spacesContext->getSpaceIdByName($owner, $space); + $itemId = $this->spacesContext->getResourceId($owner, $space, $resource); + $this->featureContext->setResponse( + HttpRequestHelper::sendRequest( + GraphHelper::getBetaFullUrl( + $this->featureContext->getBaseUrl(), + "drives/$spaceId/items/$itemId/$endpoint" + ), + $this->featureContext->getStepLineRef(), + $method, null, null, $this->getSessionHeaders($guest), - $content, + $method === 'GET' ? null : '{}', ) ); } + /** + * @param string $guest + * + * @return void + */ + #[Given('the signature of the guest session of guest :guest has been changed')] + public function theSignatureOfTheGuestSessionOfGuestHasBeenChanged(string $guest): void { + $session = $this->getGuestSession($guest); + // change the first character of the signature, the last one can carry only padding bits + $position = \strrpos($session, '.') + 1; + $this->guestSessions[$guest] = \substr_replace($session, $session[$position] === 'A' ? 'B' : 'A', $position, 1); + } + /** * @param string $guest * @param string $method @@ -416,7 +673,6 @@ public function guestSendsHttpMethodToUrlWithTheGuestSession(string $guest, stri ); } - /** * @return void */ diff --git a/tests/acceptance/features/apiGuestLink/guestLinkSecurity.feature b/tests/acceptance/features/apiGuestLink/guestLinkSecurity.feature new file mode 100644 index 0000000000..f3caf62203 --- /dev/null +++ b/tests/acceptance/features/apiGuestLink/guestLinkSecurity.feature @@ -0,0 +1,108 @@ +@email +Feature: guest link access is limited to the shared resource + As a user sharing a folder with a guest + I want the guest to only access the shared folder + So that the rest of my files stay private + + Background: + Given user "Alice" has been created with default attributes + And user "Alice" has created folder "folderToShare" + And user "Alice" has uploaded file with content "inside" to "folderToShare/inside.txt" + And user "Alice" has sent the following resource share invitation: + | resource | folderToShare | + | space | Personal | + | sharee | guest@example.org | + | shareType | guest | + | permissionsRole | Editor | + And guest "guest@example.org" has verified the guest link from the last email + + + Scenario: guest works inside the shared folder + When guest "guest@example.org" lists the content of folder "folderToShare" of space "Personal" of user "Alice" using the WebDAV API + Then the HTTP status code should be "207" + When guest "guest@example.org" uploads content "uploaded by guest" to "new.txt" inside folder "folderToShare" of space "Personal" of user "Alice" using the WebDAV API + Then the HTTP status code should be "201" + When guest "guest@example.org" creates folder "newFolder" inside folder "folderToShare" of space "Personal" of user "Alice" using the WebDAV API + Then the HTTP status code should be "201" + When guest "guest@example.org" renames "inside.txt" inside folder "folderToShare" of space "Personal" of user "Alice" to "renamed.txt" using the WebDAV API + Then the HTTP status code should be "201" + And for user "Alice" the content of the file "folderToShare/new.txt" of the space "Personal" should be "uploaded by guest" + And as "Alice" folder "folderToShare/newFolder" should exist + And as "Alice" file "folderToShare/renamed.txt" should exist + And as "Alice" file "folderToShare/inside.txt" should not exist + + + Scenario: guest cannot move a file out of the shared folder + When guest "guest@example.org" tries to move "inside.txt" inside folder "folderToShare" of space "Personal" of user "Alice" to "stolen.txt" in the root of that space using the WebDAV API + Then the HTTP status code should be "409" + And as "Alice" file "folderToShare/inside.txt" should exist + And as "Alice" file "stolen.txt" should not exist + + + Scenario: guest session gives access to all shares of the same guest + Given user "Brian" has been created with default attributes + And user "Alice" has uploaded file with content "from Alice" to "aliceFile.txt" + And user "Brian" has uploaded file with content "from Brian" to "brianFile.txt" + And user "Alice" has sent the following resource share invitation: + | resource | aliceFile.txt | + | space | Personal | + | sharee | guest@example.org | + | shareType | guest | + | permissionsRole | Viewer | + And user "Brian" has sent the following resource share invitation: + | resource | brianFile.txt | + | space | Personal | + | sharee | guest@example.org | + | shareType | guest | + | permissionsRole | Viewer | + When guest "guest@example.org" downloads file "aliceFile.txt" shared from space "Personal" of user "Alice" using the WebDAV API + Then the HTTP status code should be "200" + And the downloaded content should be "from Alice" + When guest "guest@example.org" downloads file "brianFile.txt" shared from space "Personal" of user "Brian" using the WebDAV API + Then the HTTP status code should be "200" + And the downloaded content should be "from Brian" + When guest "guest@example.org" uploads content "changed by guest" to file "aliceFile.txt" shared from space "Personal" of user "Alice" using the WebDAV API + Then the HTTP status code should be "403" + When guest "guest@example.org" lists the shares shared with them using the Graph API + Then the HTTP status code should be "200" + And the JSON data of the response should match + """ + { + "type": "object", + "required": ["value"], + "properties": { + "value": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "uniqueItems": true, + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { "enum": ["folderToShare", "aliceFile.txt", "brianFile.txt"] } + } + } + } + } + } + """ + + + Scenario Outline: guest cannot manage the share + When guest "guest@example.org" sends HTTP method "" to the Graph endpoint "" of resource "folderToShare" of space "Personal" of user "Alice" + Then the HTTP status code should be "401" + Examples: + | method | endpoint | + | GET | permissions | + | POST | invite | + | POST | createLink | + + + Scenario: guest session with an invalid signature is rejected + Given the signature of the guest session of guest "guest@example.org" has been changed + When guest "guest@example.org" lists the shares shared with them using the Graph API + Then the HTTP status code should be "401" + When guest "guest@example.org" lists the content of folder "folderToShare" of space "Personal" of user "Alice" using the WebDAV API + Then the HTTP status code should be "401" +