Skip to content

Commit 7709129

Browse files
Merge pull request #50599 from nextcloud/backport/50270/stable27
[stable27] fix(files_sharing): Respect permissions passed when creating link shares
2 parents dac41cd + 1e6a27a commit 7709129

File tree

9 files changed

+411
-301
lines changed

9 files changed

+411
-301
lines changed

.github/workflows/cypress.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ jobs:
112112
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
113113

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

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

136136
- name: Upload data dir archive
137-
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
137+
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
138138
if: failure() && matrix.containers != 'component'
139139
with:
140140
name: nc_data_${{ matrix.containers }}

.github/workflows/performance.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ jobs:
7979

8080
- name: Upload profiles
8181
if: always()
82-
uses: actions/upload-artifact@v2
82+
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
8383
with:
8484
name: profiles
8585
path: |

apps/files_sharing/lib/Controller/ShareAPIController.php

Lines changed: 109 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
namespace OCA\Files_Sharing\Controller;
4646

4747
use Exception;
48-
use OC\Files\FileInfo;
4948
use OC\Files\Storage\Wrapper\Wrapper;
5049
use OCA\Files\Helper;
5150
use OCA\Files_Sharing\Exceptions\SharingRightsException;
@@ -60,6 +59,7 @@
6059
use OCP\AppFramework\OCSController;
6160
use OCP\AppFramework\QueryException;
6261
use OCP\Constants;
62+
use OCP\Files\File;
6363
use OCP\Files\Folder;
6464
use OCP\Files\InvalidPathException;
6565
use OCP\Files\IRootFolder;
@@ -515,20 +515,21 @@ public function deleteShare(string $id): DataResponse {
515515
/**
516516
* @NoAdminRequired
517517
*
518-
* @param string $path
519-
* @param int $permissions
520-
* @param int $shareType
521-
* @param string $shareWith
522-
* @param string $publicUpload
523-
* @param string $password
524-
* @param string $sendPasswordByTalk
525-
* @param string $expireDate
526-
* @param string $label
527-
* @param string $attributes
518+
* @param string|null $path Path of the share
519+
* @param int|null $permissions Permissions for the share
520+
* @param int $shareType Type of the share
521+
* @param string|null $shareWith The entity this should be shared with
522+
* @param 'true'|'false'|null $publicUpload If public uploading is allowed (deprecated)
523+
* @param string $password Password for the share
524+
* @param string|null $sendPasswordByTalk Send the password for the share over Talk
525+
* @param string|null $expireDate The expiry date of the share in the user's timezone at 00:00.
526+
* If $expireDate is not supplied or set to `null`, the system default will be used.
527+
* @param string $note Note for the share
528+
* @param string $label Label for the share (only used in link and email)
529+
* @param string|null $attributes Additional attributes for the share
528530
*
529531
* @return DataResponse
530-
* @throws NotFoundException
531-
* @throws OCSBadRequestException
532+
* @throws OCSBadRequestException Unknown share type
532533
* @throws OCSException
533534
* @throws OCSForbiddenException
534535
* @throws OCSNotFoundException
@@ -539,8 +540,8 @@ public function createShare(
539540
string $path = null,
540541
int $permissions = null,
541542
int $shareType = -1,
542-
string $shareWith = null,
543-
string $publicUpload = 'false',
543+
?string $shareWith = null,
544+
?string $publicUpload = null,
544545
string $password = '',
545546
string $sendPasswordByTalk = null,
546547
string $expireDate = null,
@@ -549,17 +550,7 @@ public function createShare(
549550
string $attributes = null
550551
): DataResponse {
551552
$share = $this->shareManager->newShare();
552-
553-
if ($permissions === null) {
554-
if ($shareType === IShare::TYPE_LINK
555-
|| $shareType === IShare::TYPE_EMAIL) {
556-
557-
// to keep legacy default behaviour, we ignore the setting below for link shares
558-
$permissions = Constants::PERMISSION_READ;
559-
} else {
560-
$permissions = (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL);
561-
}
562-
}
553+
$hasPublicUpload = $this->getLegacyPublicUpload($publicUpload, $permissions);
563554

564555
// Verify path
565556
if ($path === null) {
@@ -578,7 +569,7 @@ public function createShare(
578569
// combine all permissions to determine if the user can share this file
579570
$nodes = $userFolder->getById($node->getId());
580571
foreach ($nodes as $nodeById) {
581-
/** @var FileInfo $fileInfo */
572+
/** @var \OC\Files\FileInfo $fileInfo */
582573
$fileInfo = $node->getFileInfo();
583574
$fileInfo['permissions'] |= $nodeById->getPermissions();
584575
}
@@ -591,17 +582,23 @@ public function createShare(
591582
throw new OCSNotFoundException($this->l->t('Could not create share'));
592583
}
593584

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

598-
// Shares always require read permissions
599-
$permissions |= Constants::PERMISSION_READ;
600-
601-
if ($node instanceof \OCP\Files\File) {
602-
// Single file shares should never have delete or create permissions
603-
$permissions &= ~Constants::PERMISSION_DELETE;
604-
$permissions &= ~Constants::PERMISSION_CREATE;
598+
// For legacy reasons the API allows to pass PERMISSIONS_ALL even for single file shares (I look at you Talk)
599+
if ($node instanceof File) {
600+
// if this is a single file share we remove the DELETE and CREATE permissions
601+
$permissions = $permissions & ~(Constants::PERMISSION_DELETE | Constants::PERMISSION_CREATE);
605602
}
606603

607604
/**
@@ -661,28 +658,7 @@ public function createShare(
661658
throw new OCSNotFoundException($this->l->t('Public link sharing is disabled by the administrator'));
662659
}
663660

664-
if ($publicUpload === 'true') {
665-
// Check if public upload is allowed
666-
if (!$this->shareManager->shareApiLinkAllowPublicUpload()) {
667-
throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator'));
668-
}
669-
670-
// Public upload can only be set for folders
671-
if ($node instanceof \OCP\Files\File) {
672-
throw new OCSNotFoundException($this->l->t('Public upload is only possible for publicly shared folders'));
673-
}
674-
675-
$permissions = Constants::PERMISSION_READ |
676-
Constants::PERMISSION_CREATE |
677-
Constants::PERMISSION_UPDATE |
678-
Constants::PERMISSION_DELETE;
679-
}
680-
681-
// TODO: It might make sense to have a dedicated setting to allow/deny converting link shares into federated ones
682-
if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
683-
$permissions |= Constants::PERMISSION_SHARE;
684-
}
685-
661+
$this->validateLinkSharePermissions($node, $permissions, $hasPublicUpload);
686662
$share->setPermissions($permissions);
687663

688664
// Set password
@@ -936,6 +912,73 @@ public function getShares(
936912
return new DataResponse($shares);
937913
}
938914

915+
private function getLinkSharePermissions(?int $permissions, ?bool $legacyPublicUpload): int {
916+
$permissions = $permissions ?? Constants::PERMISSION_READ;
917+
918+
// Legacy option handling
919+
if ($legacyPublicUpload !== null) {
920+
$permissions = $legacyPublicUpload
921+
? (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE)
922+
: Constants::PERMISSION_READ;
923+
}
924+
925+
// TODO: It might make sense to have a dedicated setting to allow/deny converting link shares into federated ones
926+
if ($this->hasPermission($permissions, Constants::PERMISSION_READ)
927+
&& $this->shareManager->outgoingServer2ServerSharesAllowed()) {
928+
$permissions |= Constants::PERMISSION_SHARE;
929+
}
930+
931+
return $permissions;
932+
}
933+
934+
/**
935+
* Helper to check for legacy "publicUpload" handling.
936+
* If the value is set to `true` or `false` then true or false are returned.
937+
* Otherwise null is returned to indicate that the option was not (or wrong) set.
938+
*
939+
* @param null|string $legacyPublicUpload The value of `publicUpload`
940+
*/
941+
private function getLegacyPublicUpload(?string $legacyPublicUpload, ?int $permissions): ?bool {
942+
if ($legacyPublicUpload === 'true') {
943+
return true;
944+
} elseif ($legacyPublicUpload === 'false') {
945+
return false;
946+
} elseif ($legacyPublicUpload === null && $permissions === null) {
947+
return false;
948+
}
949+
// Not set at all
950+
return null;
951+
}
952+
953+
/**
954+
* For link and email shares validate that only allowed combinations are set.
955+
*
956+
* @throw OCSBadRequestException If permission combination is invalid.
957+
* @throw OCSForbiddenException If public upload was forbidden by the administrator.
958+
*/
959+
private function validateLinkSharePermissions(Node $node, int $permissions, ?bool $legacyPublicUpload): void {
960+
if ($legacyPublicUpload && ($node instanceof File)) {
961+
throw new OCSBadRequestException($this->l->t('Public upload is only possible for publicly shared folders'));
962+
}
963+
964+
// We need at least READ or CREATE (file drop)
965+
if (!$this->hasPermission($permissions, Constants::PERMISSION_READ)
966+
&& !$this->hasPermission($permissions, Constants::PERMISSION_CREATE)) {
967+
throw new OCSBadRequestException($this->l->t('Share must at least have READ or CREATE permissions'));
968+
}
969+
970+
// UPDATE and DELETE require a READ permission
971+
if (!$this->hasPermission($permissions, Constants::PERMISSION_READ)
972+
&& ($this->hasPermission($permissions, Constants::PERMISSION_UPDATE) || $this->hasPermission($permissions, Constants::PERMISSION_DELETE))) {
973+
throw new OCSBadRequestException($this->l->t('Share must have READ permission if UPDATE or DELETE permission is set'));
974+
}
975+
976+
// Check if public uploading was disabled
977+
if ($this->hasPermission($permissions, Constants::PERMISSION_CREATE)
978+
&& !$this->shareManager->shareApiLinkAllowPublicUpload()) {
979+
throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator'));
980+
}
981+
}
939982

940983
/**
941984
* @param string $viewer
@@ -1125,7 +1168,6 @@ private function hasPermission(int $permissionsSet, int $permissionsToCheck): bo
11251168
return ($permissionsSet & $permissionsToCheck) === $permissionsToCheck;
11261169
}
11271170

1128-
11291171
/**
11301172
* @NoAdminRequired
11311173
*
@@ -1198,7 +1240,7 @@ public function updateShare(
11981240
$this->checkInheritedAttributes($share);
11991241

12001242
/**
1201-
* expirationdate, password and publicUpload only make sense for link shares
1243+
* expiration date, password and publicUpload only make sense for link shares
12021244
*/
12031245
if ($share->getShareType() === IShare::TYPE_LINK
12041246
|| $share->getShareType() === IShare::TYPE_EMAIL) {
@@ -1222,58 +1264,13 @@ public function updateShare(
12221264
$share->setHideDownload(false);
12231265
}
12241266

1225-
$newPermissions = null;
1226-
if ($publicUpload === 'true') {
1227-
$newPermissions = Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE;
1228-
} elseif ($publicUpload === 'false') {
1229-
$newPermissions = Constants::PERMISSION_READ;
1230-
}
1231-
1232-
if ($permissions !== null) {
1233-
$newPermissions = $permissions;
1234-
$newPermissions = $newPermissions & ~Constants::PERMISSION_SHARE;
1235-
}
1236-
1237-
if ($newPermissions !== null) {
1238-
if (!$this->hasPermission($newPermissions, Constants::PERMISSION_READ) && !$this->hasPermission($newPermissions, Constants::PERMISSION_CREATE)) {
1239-
throw new OCSBadRequestException($this->l->t('Share must at least have READ or CREATE permissions'));
1240-
}
1241-
1242-
if (!$this->hasPermission($newPermissions, Constants::PERMISSION_READ) && (
1243-
$this->hasPermission($newPermissions, Constants::PERMISSION_UPDATE) || $this->hasPermission($newPermissions, Constants::PERMISSION_DELETE)
1244-
)) {
1245-
throw new OCSBadRequestException($this->l->t('Share must have READ permission if UPDATE or DELETE permission is set'));
1246-
}
1247-
}
1248-
1249-
if (
1250-
// legacy
1251-
$newPermissions === (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE) ||
1252-
// correct
1253-
$newPermissions === (Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE)
1254-
) {
1255-
if (!$this->shareManager->shareApiLinkAllowPublicUpload()) {
1256-
throw new OCSForbiddenException($this->l->t('Public upload disabled by the administrator'));
1257-
}
1258-
1259-
if (!($share->getNode() instanceof \OCP\Files\Folder)) {
1260-
throw new OCSBadRequestException($this->l->t('Public upload is only possible for publicly shared folders'));
1261-
}
1262-
1263-
// normalize to correct public upload permissions
1264-
if ($publicUpload === 'true') {
1265-
$newPermissions = Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE;
1266-
}
1267-
}
1268-
1269-
if ($newPermissions !== null) {
1270-
// TODO: It might make sense to have a dedicated setting to allow/deny converting link shares into federated ones
1271-
if (($newPermissions & Constants::PERMISSION_READ) && $this->shareManager->outgoingServer2ServerSharesAllowed()) {
1272-
$newPermissions |= Constants::PERMISSION_SHARE;
1273-
}
1274-
1275-
$share->setPermissions($newPermissions);
1276-
$permissions = $newPermissions;
1267+
// If either manual permissions are specified or publicUpload
1268+
// then we need to also update the permissions of the share
1269+
if ($permissions !== null || $publicUpload !== null) {
1270+
$hasPublicUpload = $this->getLegacyPublicUpload($publicUpload, $permissions);
1271+
$permissions = $this->getLinkSharePermissions($permissions ?? Constants::PERMISSION_READ, $hasPublicUpload);
1272+
$this->validateLinkSharePermissions($share->getNode(), $permissions, $hasPublicUpload);
1273+
$share->setPermissions($permissions);
12771274
}
12781275

12791276
if ($password === '') {

apps/files_sharing/openapi.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,10 +1536,14 @@
15361536
{
15371537
"name": "publicUpload",
15381538
"in": "query",
1539-
"description": "If public uploading is allowed",
1539+
"description": "If public uploading is allowed (deprecated)",
15401540
"schema": {
15411541
"type": "string",
1542-
"default": "false"
1542+
"nullable": true,
1543+
"enum": [
1544+
"true",
1545+
"false"
1546+
]
15431547
}
15441548
},
15451549
{

0 commit comments

Comments
 (0)