Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable26] fix(files_sharing): Respect permissions passed when creating link shares #50598

Merged
merged 4 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/cypress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ jobs:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

- name: Upload snapshots
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
if: always()
with:
name: snapshots_${{ matrix.containers }}
Expand All @@ -123,7 +123,7 @@ jobs:
run: docker logs nextcloud-cypress-tests-${{ env.APP_NAME }} > nextcloud.log

- name: Upload NC logs
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
if: failure() && matrix.containers != 'component'
with:
name: nc_logs_${{ matrix.containers }}
Expand All @@ -134,7 +134,7 @@ jobs:
run: docker exec nextcloud-cypress-tests-server tar -cvjf - data > data.tar

- name: Upload data dir archive
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
if: failure() && matrix.containers != 'component'
with:
name: nc_data_${{ matrix.containers }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/performance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:

- name: Upload profiles
if: always()
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: profiles
path: |
Expand Down
220 changes: 109 additions & 111 deletions apps/files_sharing/lib/Controller/ShareAPIController.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
namespace OCA\Files_Sharing\Controller;

use Exception;
use OC\Files\FileInfo;
use OC\Files\Storage\Wrapper\Wrapper;
use OCA\Files\Helper;
use OCA\Files_Sharing\Exceptions\SharingRightsException;
Expand All @@ -60,6 +59,7 @@
use OCP\AppFramework\OCSController;
use OCP\AppFramework\QueryException;
use OCP\Constants;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
Expand Down Expand Up @@ -549,20 +549,21 @@ public function deleteShare(string $id): DataResponse {
/**
* @NoAdminRequired
*
* @param string $path
* @param int $permissions
* @param int $shareType
* @param string $shareWith
* @param string $publicUpload
* @param string $password
* @param string $sendPasswordByTalk
* @param string $expireDate
* @param string $label
* @param string $attributes
* @param string|null $path Path of the share
* @param int|null $permissions Permissions for the share
* @param int $shareType Type of the share
* @param string|null $shareWith The entity this should be shared with
* @param 'true'|'false'|null $publicUpload If public uploading is allowed (deprecated)
* @param string $password Password for the share
* @param string|null $sendPasswordByTalk Send the password for the share over Talk
* @param string $expireDate The expiry date of the share in the user's timezone at 00:00.
* If $expireDate is not supplied or set to `null`, the system default will be used.
* @param string $note Note for the share
* @param string $label Label for the share (only used in link and email)
* @param string|null $attributes Additional attributes for the share
*
* @return DataResponse
* @throws NotFoundException
* @throws OCSBadRequestException
* @throws OCSBadRequestException Unknown share type
* @throws OCSException
* @throws OCSForbiddenException
* @throws OCSNotFoundException
Expand All @@ -573,8 +574,8 @@ public function createShare(
string $path = null,
int $permissions = null,
int $shareType = -1,
string $shareWith = null,
string $publicUpload = 'false',
?string $shareWith = null,
?string $publicUpload = null,
string $password = '',
string $sendPasswordByTalk = null,
string $expireDate = '',
Expand All @@ -583,16 +584,7 @@ public function createShare(
string $attributes = null
): DataResponse {
$share = $this->shareManager->newShare();

if ($permissions === null) {
if ($shareType === IShare::TYPE_LINK
|| $shareType === IShare::TYPE_EMAIL) {
// to keep legacy default behaviour, we ignore the setting below for link shares
$permissions = Constants::PERMISSION_READ;
} else {
$permissions = (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL);
}
}
$hasPublicUpload = $this->getLegacyPublicUpload($publicUpload, $permissions);

// Verify path
if ($path === null) {
Expand All @@ -611,7 +603,7 @@ public function createShare(
// combine all permissions to determine if the user can share this file
$nodes = $userFolder->getById($node->getId());
foreach ($nodes as $nodeById) {
/** @var FileInfo $fileInfo */
/** @var \OC\Files\FileInfo $fileInfo */
$fileInfo = $node->getFileInfo();
$fileInfo['permissions'] |= $nodeById->getPermissions();
}
Expand All @@ -624,17 +616,23 @@ public function createShare(
throw new OCSNotFoundException($this->l->t('Could not create share'));
}

if ($permissions < 0 || $permissions > Constants::PERMISSION_ALL) {
throw new OCSNotFoundException($this->l->t('Invalid permissions'));
// Set permissions
if ($shareType === IShare::TYPE_LINK || $shareType === IShare::TYPE_EMAIL) {
$permissions = $this->getLinkSharePermissions($permissions, $hasPublicUpload);
$this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload);
} else {
// Use default permissions only for non-link shares to keep legacy behavior
if ($permissions === null) {
$permissions = (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL);
}
// Non-link shares always require read permissions (link shares could be file drop)
$permissions |= Constants::PERMISSION_READ;
}

// Shares always require read permissions
$permissions |= Constants::PERMISSION_READ;

if ($node instanceof \OCP\Files\File) {
// Single file shares should never have delete or create permissions
$permissions &= ~Constants::PERMISSION_DELETE;
$permissions &= ~Constants::PERMISSION_CREATE;
// For legacy reasons the API allows to pass PERMISSIONS_ALL even for single file shares (I look at you Talk)
if ($node instanceof File) {
// if this is a single file share we remove the DELETE and CREATE permissions
$permissions = $permissions & ~(Constants::PERMISSION_DELETE | Constants::PERMISSION_CREATE);
}

/**
Expand Down Expand Up @@ -678,28 +676,7 @@ public function createShare(
throw new OCSNotFoundException($this->l->t('Public link sharing is disabled by the administrator'));
}

if ($publicUpload === 'true') {
// Check if public upload is allowed
if (!$this->shareManager->shareApiLinkAllowPublicUpload()) {
throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator'));
}

// Public upload can only be set for folders
if ($node instanceof \OCP\Files\File) {
throw new OCSNotFoundException($this->l->t('Public upload is only possible for publicly shared folders'));
}

$permissions = Constants::PERMISSION_READ |
Constants::PERMISSION_CREATE |
Constants::PERMISSION_UPDATE |
Constants::PERMISSION_DELETE;
}

// TODO: It might make sense to have a dedicated setting to allow/deny converting link shares into federated ones
if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
$permissions |= Constants::PERMISSION_SHARE;
}

$this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload);
$share->setPermissions($permissions);

// Set password
Expand Down Expand Up @@ -980,6 +957,73 @@ public function getShares(
return new DataResponse($shares);
}

private function getLinkSharePermissions(?int $permissions, ?bool $legacyPublicUpload): int {
$permissions = $permissions ?? Constants::PERMISSION_READ;

// Legacy option handling
if ($legacyPublicUpload !== null) {
$permissions = $legacyPublicUpload
? (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE)
: Constants::PERMISSION_READ;
}

// TODO: It might make sense to have a dedicated setting to allow/deny converting link shares into federated ones
if ($this->hasPermission($permissions, Constants::PERMISSION_READ)
&& $this->shareManager->outgoingServer2ServerSharesAllowed()) {
$permissions |= Constants::PERMISSION_SHARE;
}

return $permissions;
}

/**
* Helper to check for legacy "publicUpload" handling.
* If the value is set to `true` or `false` then true or false are returned.
* Otherwise null is returned to indicate that the option was not (or wrong) set.
*
* @param null|string $legacyPublicUpload The value of `publicUpload`
*/
private function getLegacyPublicUpload(?string $legacyPublicUpload, ?int $permissions): ?bool {
if ($legacyPublicUpload === 'true') {
return true;
} elseif ($legacyPublicUpload === 'false') {
return false;
} elseif ($legacyPublicUpload === null && $permissions === null) {
return false;
}
// Not set at all
return null;
}

/**
* For link and email shares validate that only allowed combinations are set.
*
* @throw OCSBadRequestException If permission combination is invalid.
* @throw OCSForbiddenException If public upload was forbidden by the administrator.
*/
private function validateLinkSharePermissions(Node $node, int $permissions, ?bool $legacyPublicUpload): void {
if ($legacyPublicUpload && ($node instanceof File)) {
throw new OCSBadRequestException($this->l->t('Public upload is only possible for publicly shared folders'));
}

// We need at least READ or CREATE (file drop)
if (!$this->hasPermission($permissions, Constants::PERMISSION_READ)
&& !$this->hasPermission($permissions, Constants::PERMISSION_CREATE)) {
throw new OCSBadRequestException($this->l->t('Share must at least have READ or CREATE permissions'));
}

// UPDATE and DELETE require a READ permission
if (!$this->hasPermission($permissions, Constants::PERMISSION_READ)
&& ($this->hasPermission($permissions, Constants::PERMISSION_UPDATE) || $this->hasPermission($permissions, Constants::PERMISSION_DELETE))) {
throw new OCSBadRequestException($this->l->t('Share must have READ permission if UPDATE or DELETE permission is set'));
}

// Check if public uploading was disabled
if ($this->hasPermission($permissions, Constants::PERMISSION_CREATE)
&& !$this->shareManager->shareApiLinkAllowPublicUpload()) {
throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator'));
}
}

/**
* @param string $viewer
Expand Down Expand Up @@ -1165,7 +1209,6 @@ private function hasPermission(int $permissionsSet, int $permissionsToCheck): bo
return ($permissionsSet & $permissionsToCheck) === $permissionsToCheck;
}


/**
* @NoAdminRequired
*
Expand Down Expand Up @@ -1238,7 +1281,7 @@ public function updateShare(
$this->checkInheritedAttributes($share);

/**
* expirationdate, password and publicUpload only make sense for link shares
* expiration date, password and publicUpload only make sense for link shares
*/
if ($share->getShareType() === IShare::TYPE_LINK
|| $share->getShareType() === IShare::TYPE_EMAIL) {
Expand All @@ -1261,58 +1304,13 @@ public function updateShare(
$share->setHideDownload(false);
}

$newPermissions = null;
if ($publicUpload === 'true') {
$newPermissions = Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE;
} elseif ($publicUpload === 'false') {
$newPermissions = Constants::PERMISSION_READ;
}

if ($permissions !== null) {
$newPermissions = $permissions;
$newPermissions = $newPermissions & ~Constants::PERMISSION_SHARE;
}

if ($newPermissions !== null) {
if (!$this->hasPermission($newPermissions, Constants::PERMISSION_READ) && !$this->hasPermission($newPermissions, Constants::PERMISSION_CREATE)) {
throw new OCSBadRequestException($this->l->t('Share must at least have READ or CREATE permissions'));
}

if (!$this->hasPermission($newPermissions, Constants::PERMISSION_READ) && (
$this->hasPermission($newPermissions, Constants::PERMISSION_UPDATE) || $this->hasPermission($newPermissions, Constants::PERMISSION_DELETE)
)) {
throw new OCSBadRequestException($this->l->t('Share must have READ permission if UPDATE or DELETE permission is set'));
}
}

if (
// legacy
$newPermissions === (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE) ||
// correct
$newPermissions === (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE)
) {
if (!$this->shareManager->shareApiLinkAllowPublicUpload()) {
throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator'));
}

if (!($share->getNode() instanceof \OCP\Files\Folder)) {
throw new OCSBadRequestException($this->l->t('Public upload is only possible for publicly shared folders'));
}

// normalize to correct public upload permissions
if ($publicUpload === 'true') {
$newPermissions = Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE;
}
}

if ($newPermissions !== null) {
// TODO: It might make sense to have a dedicated setting to allow/deny converting link shares into federated ones
if (($newPermissions & Constants::PERMISSION_READ) && $this->shareManager->outgoingServer2ServerSharesAllowed()) {
$newPermissions |= Constants::PERMISSION_SHARE;
}

$share->setPermissions($newPermissions);
$permissions = $newPermissions;
// If either manual permissions are specified or publicUpload
// then we need to also update the permissions of the share
if ($permissions !== null || $publicUpload !== null) {
$hasPublicUpload = $this->getLegacyPublicUpload($publicUpload, $permissions);
$permissions = $this->getLinkSharePermissions($permissions ?? Constants::PERMISSION_READ, $hasPublicUpload);
$this->validateLinkSharePermissions($share->getNode(), $permissions, $hasPublicUpload);
$share->setPermissions($permissions);
}

if ($password === '') {
Expand Down
Loading
Loading