tests: check that a guest can only access the shared resource (#3752)

Make sure the guest session gives no access outside of the shared folder.
This commit is contained in:
Viktor Scharf authored and GitHub committed 2026-10-19 20:55:29 +02:00
1 parent 38262408d6
commit d0958f04c2
2 files changed
+385 -21

No files matched your search

+277 -21
View File
@@ -29,6 +29,7 @@ use PHPUnit\Framework\Assert;
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 @@ class GuestLinkContext implements Context {
"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 @@ class GuestLinkContext implements Context {
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 @@ class GuestLinkContext implements Context {
* @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 @@ class GuestLinkContext implements Context {
*/
#[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 @@ class GuestLinkContext implements Context {
* @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"),
WebDavHelper::makeDavRequest(
$this->featureContext->getBaseUrl(),
null,
null,
'GET',
'',
\array_merge($this->getSessionHeaders($guest), []),
$fileId,
$this->featureContext->getStepLineRef(),
null,
null,
$this->getSessionHeaders($guest),
WebDavHelper::DAV_VERSION_SPACES
)
);
}
@@ -381,18 +404,252 @@ class GuestLinkContext implements Context {
): void {
$fileId = $this->spacesContext->getResourceId($owner, $space, $resource);
$this->featureContext->setResponse(
HttpRequestHelper::sendRequest(
$this->featureContext->getBaseUrl() . "/" . WebDavHelper::prefixRemotePhp("dav/spaces/$fileId"),
$this->featureContext->getStepLineRef(),
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 @@ class GuestLinkContext implements Context {
);
}
/**
* @return void
*/
@@ -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 "<method>" to the Graph endpoint "<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"