diff --git a/apps/files_sharing/lib/Capabilities.php b/apps/files_sharing/lib/Capabilities.php index 06aa1271c8f8d..7b911cb74ddaf 100644 --- a/apps/files_sharing/lib/Capabilities.php +++ b/apps/files_sharing/lib/Capabilities.php @@ -8,6 +8,7 @@ namespace OCA\Files_Sharing; use OC\Core\AppInfo\ConfigLexicon; +use OCA\Files_Sharing\Config\ConfigLexicon as SharingConfigLexicon; use OCP\App\IAppManager; use OCP\Capabilities\ICapability; use OCP\Constants; @@ -77,6 +78,7 @@ public function __construct( * }, * }, * default_permissions?: int, + * exclude_reshare_from_edit?: bool, * federation: array{ * outgoing: bool, * incoming: bool, @@ -159,6 +161,7 @@ public function getCapabilities() { $res['group']['enabled'] = $this->shareManager->allowGroupSharing(); $res['group']['expire_date']['enabled'] = true; $res['default_permissions'] = (int)$this->config->getAppValue('core', 'shareapi_default_permissions', (string)Constants::PERMISSION_ALL); + $res['exclude_reshare_from_edit'] = $this->appConfig->getValueBool('files_sharing', SharingConfigLexicon::EXCLUDE_RESHARE_FROM_EDIT); } //Federated sharing diff --git a/apps/files_sharing/lib/Config/ConfigLexicon.php b/apps/files_sharing/lib/Config/ConfigLexicon.php index c2743a2c4ce16..c063153765e26 100644 --- a/apps/files_sharing/lib/Config/ConfigLexicon.php +++ b/apps/files_sharing/lib/Config/ConfigLexicon.php @@ -23,6 +23,7 @@ class ConfigLexicon implements ILexicon { public const SHOW_FEDERATED_AS_INTERNAL = 'show_federated_shares_as_internal'; public const SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL = 'show_federated_shares_to_trusted_servers_as_internal'; + public const EXCLUDE_RESHARE_FROM_EDIT = 'shareapi_exclude_reshare_from_edit'; public function getStrictness(): Strictness { return Strictness::IGNORE; @@ -32,6 +33,7 @@ public function getAppConfigs(): array { return [ new Entry(self::SHOW_FEDERATED_AS_INTERNAL, ValueType::BOOL, false, 'shows federated shares as internal shares', true), new Entry(self::SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL, ValueType::BOOL, false, 'shows federated shares to trusted servers as internal shares', true), + new Entry(self::EXCLUDE_RESHARE_FROM_EDIT, ValueType::BOOL, false, 'Exclude reshare permission from "Allow editing" bundled permissions'), ]; } diff --git a/apps/files_sharing/openapi.json b/apps/files_sharing/openapi.json index dc89752f091b9..257250434f134 100644 --- a/apps/files_sharing/openapi.json +++ b/apps/files_sharing/openapi.json @@ -189,6 +189,9 @@ "type": "integer", "format": "int64" }, + "exclude_reshare_from_edit": { + "type": "boolean" + }, "federation": { "type": "object", "required": [ diff --git a/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue b/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue index 342ad4db829a5..473ad15e8ea86 100644 --- a/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue +++ b/apps/files_sharing/src/components/SharingEntryQuickShareSelect.vue @@ -41,7 +41,7 @@ import DropdownIcon from 'vue-material-design-icons/TriangleSmallDown.vue' import IconTune from 'vue-material-design-icons/Tune.vue' import { ATOMIC_PERMISSIONS, - BUNDLED_PERMISSIONS, + getBundledPermissions, } from '../lib/SharePermissionsToolBox.js' import ShareDetails from '../mixins/ShareDetails.js' import SharesMixin from '../mixins/SharesMixin.js' @@ -93,14 +93,19 @@ export default { return t('files_sharing', 'Custom permissions') }, + bundledPermissions() { + return getBundledPermissions(this.config.excludeReshareFromEdit) + }, + preSelectedOption() { // We remove the share permission for the comparison as it is not relevant for bundled permissions. const permissionsWithoutShare = this.share.permissions & ~ATOMIC_PERMISSIONS.SHARE - if (permissionsWithoutShare === BUNDLED_PERMISSIONS.READ_ONLY) { + const basePermissions = getBundledPermissions(true) + if (permissionsWithoutShare === basePermissions.READ_ONLY) { return this.canViewText - } else if (permissionsWithoutShare === BUNDLED_PERMISSIONS.ALL || permissionsWithoutShare === BUNDLED_PERMISSIONS.ALL_FILE) { + } else if (permissionsWithoutShare === basePermissions.ALL || permissionsWithoutShare === basePermissions.ALL_FILE) { return this.canEditText - } else if (permissionsWithoutShare === BUNDLED_PERMISSIONS.FILE_DROP) { + } else if (permissionsWithoutShare === basePermissions.FILE_DROP) { return this.fileDropText } @@ -140,14 +145,14 @@ export default { dropDownPermissionValue() { switch (this.selectedOption) { case this.canEditText: - return this.isFolder ? BUNDLED_PERMISSIONS.ALL : BUNDLED_PERMISSIONS.ALL_FILE + return this.isFolder ? this.bundledPermissions.ALL : this.bundledPermissions.ALL_FILE case this.fileDropText: - return BUNDLED_PERMISSIONS.FILE_DROP + return this.bundledPermissions.FILE_DROP case this.customPermissionsText: return 'custom' case this.canViewText: default: - return BUNDLED_PERMISSIONS.READ_ONLY + return this.bundledPermissions.READ_ONLY } }, }, diff --git a/apps/files_sharing/src/lib/SharePermissionsToolBox.js b/apps/files_sharing/src/lib/SharePermissionsToolBox.js index bdf57b37ce710..3638d94f5f607 100644 --- a/apps/files_sharing/src/lib/SharePermissionsToolBox.js +++ b/apps/files_sharing/src/lib/SharePermissionsToolBox.js @@ -12,12 +12,29 @@ export const ATOMIC_PERMISSIONS = { SHARE: 16, } -export const BUNDLED_PERMISSIONS = { +const BUNDLED_PERMISSIONS = { READ_ONLY: ATOMIC_PERMISSIONS.READ, UPLOAD_AND_UPDATE: ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.DELETE, FILE_DROP: ATOMIC_PERMISSIONS.CREATE, - ALL: ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.DELETE, - ALL_FILE: ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.READ, + ALL: ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.DELETE | ATOMIC_PERMISSIONS.SHARE, + ALL_FILE: ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.SHARE, +} + +/** + * Get bundled permissions based on config. + * + * @param {boolean} excludeShare - Whether to exclude SHARE permission from ALL and ALL_FILE bundles. + * @return {object} + */ +export function getBundledPermissions(excludeShare = false) { + if (excludeShare) { + return { + ...BUNDLED_PERMISSIONS, + ALL: BUNDLED_PERMISSIONS.ALL & ~ATOMIC_PERMISSIONS.SHARE, + ALL_FILE: BUNDLED_PERMISSIONS.ALL_FILE & ~ATOMIC_PERMISSIONS.SHARE, + } + } + return BUNDLED_PERMISSIONS } /** diff --git a/apps/files_sharing/src/lib/SharePermissionsToolBox.spec.js b/apps/files_sharing/src/lib/SharePermissionsToolBox.spec.js index eb262769f63c3..14ac7bfbbbb74 100644 --- a/apps/files_sharing/src/lib/SharePermissionsToolBox.spec.js +++ b/apps/files_sharing/src/lib/SharePermissionsToolBox.spec.js @@ -6,21 +6,23 @@ import { describe, expect, test } from 'vitest' import { addPermissions, ATOMIC_PERMISSIONS, - BUNDLED_PERMISSIONS, canTogglePermissions, + getBundledPermissions, hasPermissions, permissionsSetIsValid, subtractPermissions, togglePermissions, } from '../lib/SharePermissionsToolBox.js' +const BUNDLED_PERMISSIONS = getBundledPermissions() + describe('SharePermissionsToolBox', () => { test('Adding permissions', () => { expect(addPermissions(ATOMIC_PERMISSIONS.NONE, ATOMIC_PERMISSIONS.NONE)).toBe(ATOMIC_PERMISSIONS.NONE) expect(addPermissions(ATOMIC_PERMISSIONS.NONE, ATOMIC_PERMISSIONS.READ)).toBe(ATOMIC_PERMISSIONS.READ) expect(addPermissions(ATOMIC_PERMISSIONS.READ, ATOMIC_PERMISSIONS.READ)).toBe(ATOMIC_PERMISSIONS.READ) expect(addPermissions(ATOMIC_PERMISSIONS.READ, ATOMIC_PERMISSIONS.UPDATE)).toBe(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE) - expect(addPermissions(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE, ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.DELETE)).toBe(BUNDLED_PERMISSIONS.ALL) + expect(addPermissions(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE, ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.DELETE | ATOMIC_PERMISSIONS.SHARE)).toBe(BUNDLED_PERMISSIONS.ALL) expect(addPermissions(BUNDLED_PERMISSIONS.ALL, ATOMIC_PERMISSIONS.READ)).toBe(BUNDLED_PERMISSIONS.ALL) expect(addPermissions(BUNDLED_PERMISSIONS.ALL, ATOMIC_PERMISSIONS.NONE)).toBe(BUNDLED_PERMISSIONS.ALL) }) @@ -32,7 +34,7 @@ describe('SharePermissionsToolBox', () => { expect(subtractPermissions(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE, ATOMIC_PERMISSIONS.UPDATE)).toBe(ATOMIC_PERMISSIONS.READ) expect(subtractPermissions(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE, ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.DELETE)).toBe(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE) expect(subtractPermissions(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE, ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.DELETE)).toBe(ATOMIC_PERMISSIONS.READ) - expect(subtractPermissions(BUNDLED_PERMISSIONS.ALL, ATOMIC_PERMISSIONS.READ)).toBe(ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.DELETE) + expect(subtractPermissions(BUNDLED_PERMISSIONS.ALL, ATOMIC_PERMISSIONS.READ)).toBe(ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.DELETE | ATOMIC_PERMISSIONS.SHARE) }) test('Has permissions', () => { @@ -45,8 +47,8 @@ describe('SharePermissionsToolBox', () => { }) test('Toggle permissions', () => { - expect(togglePermissions(BUNDLED_PERMISSIONS.ALL, BUNDLED_PERMISSIONS.UPLOAD_AND_UPDATE)).toBe(ATOMIC_PERMISSIONS.NONE) - expect(togglePermissions(BUNDLED_PERMISSIONS.ALL, BUNDLED_PERMISSIONS.FILE_DROP)).toBe(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.DELETE) + expect(togglePermissions(BUNDLED_PERMISSIONS.ALL, BUNDLED_PERMISSIONS.UPLOAD_AND_UPDATE)).toBe(ATOMIC_PERMISSIONS.SHARE) + expect(togglePermissions(BUNDLED_PERMISSIONS.ALL, BUNDLED_PERMISSIONS.FILE_DROP)).toBe(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.DELETE | ATOMIC_PERMISSIONS.SHARE) expect(togglePermissions(BUNDLED_PERMISSIONS.ALL, ATOMIC_PERMISSIONS.NONE)).toBe(BUNDLED_PERMISSIONS.ALL) expect(togglePermissions(ATOMIC_PERMISSIONS.NONE, BUNDLED_PERMISSIONS.ALL)).toBe(BUNDLED_PERMISSIONS.ALL) expect(togglePermissions(ATOMIC_PERMISSIONS.READ, BUNDLED_PERMISSIONS.ALL)).toBe(BUNDLED_PERMISSIONS.ALL) @@ -76,4 +78,70 @@ describe('SharePermissionsToolBox', () => { expect(canTogglePermissions(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.UPDATE, ATOMIC_PERMISSIONS.CREATE)).toBe(true) expect(canTogglePermissions(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.DELETE, ATOMIC_PERMISSIONS.CREATE)).toBe(true) }) + + test('Get bundled permissions with SHARE included (default)', () => { + const permissions = getBundledPermissions() + expect(permissions.READ_ONLY).toBe(BUNDLED_PERMISSIONS.READ_ONLY) + expect(permissions.FILE_DROP).toBe(BUNDLED_PERMISSIONS.FILE_DROP) + expect(permissions.UPLOAD_AND_UPDATE).toBe(BUNDLED_PERMISSIONS.UPLOAD_AND_UPDATE) + expect(permissions.ALL).toBe(BUNDLED_PERMISSIONS.ALL) + expect(permissions.ALL_FILE).toBe(BUNDLED_PERMISSIONS.ALL_FILE) + expect(permissions.ALL).toBe(31) + expect(permissions.ALL_FILE).toBe(19) + expect(hasPermissions(permissions.ALL, ATOMIC_PERMISSIONS.SHARE)).toBe(true) + expect(hasPermissions(permissions.ALL_FILE, ATOMIC_PERMISSIONS.SHARE)).toBe(true) + }) + + test('Get bundled permissions without SHARE (excludeShare=true)', () => { + const permissions = getBundledPermissions(true) + expect(permissions.READ_ONLY).toBe(BUNDLED_PERMISSIONS.READ_ONLY) + expect(permissions.FILE_DROP).toBe(BUNDLED_PERMISSIONS.FILE_DROP) + expect(permissions.UPLOAD_AND_UPDATE).toBe(BUNDLED_PERMISSIONS.UPLOAD_AND_UPDATE) + expect(permissions.ALL).toBe(BUNDLED_PERMISSIONS.ALL & ~ATOMIC_PERMISSIONS.SHARE) + expect(permissions.ALL_FILE).toBe(BUNDLED_PERMISSIONS.ALL_FILE & ~ATOMIC_PERMISSIONS.SHARE) + expect(permissions.ALL).toBe(15) + expect(permissions.ALL_FILE).toBe(3) + expect(hasPermissions(permissions.ALL, ATOMIC_PERMISSIONS.SHARE)).toBe(false) + expect(hasPermissions(permissions.ALL_FILE, ATOMIC_PERMISSIONS.SHARE)).toBe(false) + }) + + test('Operations with bundled permissions including SHARE', () => { + const permissionsWithShare = getBundledPermissions(false) + const permissionsWithoutShare = getBundledPermissions(true) + + // Adding permissions to ALL with SHARE should preserve SHARE + expect(addPermissions(permissionsWithShare.ALL, ATOMIC_PERMISSIONS.READ)).toBe(permissionsWithShare.ALL) + + // Subtracting READ from ALL with SHARE should leave UPDATE | CREATE | DELETE | SHARE + expect(subtractPermissions(permissionsWithShare.ALL, ATOMIC_PERMISSIONS.READ)) + .toBe(ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.DELETE | ATOMIC_PERMISSIONS.SHARE) + + // Toggle UPLOAD_AND_UPDATE from ALL with SHARE should leave only SHARE + expect(togglePermissions(permissionsWithShare.ALL, BUNDLED_PERMISSIONS.UPLOAD_AND_UPDATE)) + .toBe(ATOMIC_PERMISSIONS.SHARE) + + // Toggle FILE_DROP from ALL with SHARE + expect(togglePermissions(permissionsWithShare.ALL, BUNDLED_PERMISSIONS.FILE_DROP)) + .toBe(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.DELETE | ATOMIC_PERMISSIONS.SHARE) + + // BUNDLED_PERMISSIONS.ALL already includes SHARE + expect(BUNDLED_PERMISSIONS.ALL).toBe(permissionsWithShare.ALL) + + // Subtracting SHARE from ALL with SHARE should equal ALL without SHARE + expect(subtractPermissions(permissionsWithShare.ALL, ATOMIC_PERMISSIONS.SHARE)).toBe(permissionsWithoutShare.ALL) + }) + + test('Operations with bundled permissions for files including SHARE', () => { + const permissionsWithShare = getBundledPermissions(false) + const permissionsWithoutShare = getBundledPermissions(true) + + // ALL_FILE with SHARE should be READ | UPDATE | SHARE + expect(permissionsWithShare.ALL_FILE).toBe(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE | ATOMIC_PERMISSIONS.SHARE) + + // Subtracting SHARE from ALL_FILE with SHARE should equal ALL_FILE without SHARE + expect(subtractPermissions(permissionsWithShare.ALL_FILE, ATOMIC_PERMISSIONS.SHARE)).toBe(permissionsWithoutShare.ALL_FILE) + + // BUNDLED_PERMISSIONS.ALL_FILE already includes SHARE + expect(BUNDLED_PERMISSIONS.ALL_FILE).toBe(permissionsWithShare.ALL_FILE) + }) }) diff --git a/apps/files_sharing/src/mixins/SharesMixin.js b/apps/files_sharing/src/mixins/SharesMixin.js index e94bf80e401fe..f5e3902814f86 100644 --- a/apps/files_sharing/src/mixins/SharesMixin.js +++ b/apps/files_sharing/src/mixins/SharesMixin.js @@ -12,7 +12,7 @@ import PQueue from 'p-queue' import { fetchNode } from '../../../files/src/services/WebdavClient.ts' import { ATOMIC_PERMISSIONS, - BUNDLED_PERMISSIONS, + getBundledPermissions, } from '../lib/SharePermissionsToolBox.js' import Share from '../models/Share.ts' import Config from '../services/ConfigService.ts' @@ -138,11 +138,12 @@ export default { return this.config.isDefaultInternalExpireDateEnforced }, hasCustomPermissions() { + const basePermissions = getBundledPermissions(true) const bundledPermissions = [ - BUNDLED_PERMISSIONS.ALL, - BUNDLED_PERMISSIONS.ALL_FILE, - BUNDLED_PERMISSIONS.READ_ONLY, - BUNDLED_PERMISSIONS.FILE_DROP, + basePermissions.ALL, + basePermissions.ALL_FILE, + basePermissions.READ_ONLY, + basePermissions.FILE_DROP, ] const permissionsWithoutShare = this.share.permissions & ~ATOMIC_PERMISSIONS.SHARE return !bundledPermissions.includes(permissionsWithoutShare) diff --git a/apps/files_sharing/src/services/ConfigService.ts b/apps/files_sharing/src/services/ConfigService.ts index 71824bc855b6e..7d3a24672ea6a 100644 --- a/apps/files_sharing/src/services/ConfigService.ts +++ b/apps/files_sharing/src/services/ConfigService.ts @@ -53,6 +53,7 @@ type FileSharingCapabilities = { } } default_permissions: number + exclude_reshare_from_edit: boolean federation: { outgoing: boolean incoming: boolean @@ -103,6 +104,13 @@ export default class Config { return this._capabilities.files_sharing?.default_permissions } + /** + * Should SHARE permission be excluded from "Allow editing" bundled permissions + */ + get excludeReshareFromEdit(): boolean { + return this._capabilities.files_sharing?.exclude_reshare_from_edit === true + } + /** * Is public upload allowed on link shares ? * This covers File request and Full upload/edit option. diff --git a/apps/files_sharing/src/views/SharingDetailsTab.vue b/apps/files_sharing/src/views/SharingDetailsTab.vue index 896d90442eebe..9830fe88a6aca 100644 --- a/apps/files_sharing/src/views/SharingDetailsTab.vue +++ b/apps/files_sharing/src/views/SharingDetailsTab.vue @@ -331,7 +331,7 @@ import SidebarTabExternalAction from '../components/SidebarTabExternal/SidebarTa import SidebarTabExternalActionLegacy from '../components/SidebarTabExternal/SidebarTabExternalActionLegacy.vue' import { ATOMIC_PERMISSIONS, - BUNDLED_PERMISSIONS, + getBundledPermissions, hasPermissions, } from '../lib/SharePermissionsToolBox.js' import ShareRequests from '../mixins/ShareRequests.js' @@ -390,12 +390,11 @@ export default { data() { return { writeNoteToRecipientIsChecked: false, - sharingPermission: BUNDLED_PERMISSIONS.ALL.toString(), - revertSharingPermission: BUNDLED_PERMISSIONS.ALL.toString(), + sharingPermission: getBundledPermissions().ALL.toString(), + revertSharingPermission: getBundledPermissions().ALL.toString(), setCustomPermissions: false, passwordError: false, advancedSectionAccordionExpanded: false, - bundledPermissions: BUNDLED_PERMISSIONS, isFirstComponentLoad: true, test: false, creating: false, @@ -443,6 +442,10 @@ export default { } }, + bundledPermissions() { + return getBundledPermissions(this.config.excludeReshareFromEdit) + }, + allPermissions() { return this.isFolder ? this.bundledPermissions.ALL.toString() : this.bundledPermissions.ALL_FILE.toString() }, @@ -1022,9 +1025,10 @@ export default { if (this.isNewShare) { const defaultPermissions = this.config.defaultPermissions const permissionsWithoutShare = defaultPermissions & ~ATOMIC_PERMISSIONS.SHARE - if (permissionsWithoutShare === BUNDLED_PERMISSIONS.READ_ONLY - || permissionsWithoutShare === BUNDLED_PERMISSIONS.ALL - || permissionsWithoutShare === BUNDLED_PERMISSIONS.ALL_FILE) { + const basePermissions = getBundledPermissions(true) + if (permissionsWithoutShare === basePermissions.READ_ONLY + || permissionsWithoutShare === basePermissions.ALL + || permissionsWithoutShare === basePermissions.ALL_FILE) { this.sharingPermission = permissionsWithoutShare.toString() } else { this.sharingPermission = 'custom' @@ -1075,9 +1079,9 @@ export default { this.share.permissions = sharePermissionsSet } - if (!this.isFolder && this.share.permissions === BUNDLED_PERMISSIONS.ALL) { + if (!this.isFolder && this.share.permissions === this.bundledPermissions.ALL) { // It's not possible to create an existing file. - this.share.permissions = BUNDLED_PERMISSIONS.ALL_FILE + this.share.permissions = this.bundledPermissions.ALL_FILE } if (!this.writeNoteToRecipientIsChecked) { this.share.note = '' diff --git a/cypress/e2e/files_sharing/share-permissions-bundle.cy.ts b/cypress/e2e/files_sharing/share-permissions-bundle.cy.ts new file mode 100644 index 0000000000000..dec8173c4a9c1 --- /dev/null +++ b/cypress/e2e/files_sharing/share-permissions-bundle.cy.ts @@ -0,0 +1,111 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { User } from '@nextcloud/e2e-test-server/cypress' + +import { openSharingPanel } from './FilesSharingUtils.ts' + +describe('files_sharing: Share permissions bundle configuration', () => { + let alice: User + let bob: User + + before(() => { + cy.createRandomUser().then(($user) => { + alice = $user + }) + cy.createRandomUser().then(($user) => { + bob = $user + }) + }) + + beforeEach(() => { + cy.runOccCommand('config:app:delete files_sharing shareapi_exclude_reshare_from_edit') + }) + + after(() => { + cy.runOccCommand('config:app:delete files_sharing shareapi_exclude_reshare_from_edit') + }) + + /** + * Helper to create a user share and select "Allow editing" + */ + function createUserShareWithEdit(itemName: string) { + openSharingPanel(itemName) + + cy.get('#app-sidebar-vue').within(() => { + cy.intercept('GET', '**/apps/files_sharing/api/v1/sharees?*').as('shareeSearch') + cy.findByRole('combobox', { name: /Search for internal recipients/i }) + .type(`{selectAll}${bob.userId}`) + cy.wait('@shareeSearch') + }) + + cy.get(`[user="${bob.userId}"]`).click() + + // Select "Allow editing" permission bundle + cy.get('[data-cy-files-sharing-share-permissions-bundle]').should('be.visible') + cy.get('[data-cy-files-sharing-share-permissions-bundle="upload-edit"]').click() + + cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare') + cy.findByRole('button', { name: 'Save share' }).click() + + return cy.wait('@createShare') + } + + describe('Default behavior (SHARE included in edit)', () => { + it('Creates user share with "Allow editing" with SHARE permission for folders', () => { + const folderName = 'test-folder-with-share' + cy.mkdir(alice, `/${folderName}`) + cy.login(alice) + cy.visit('/apps/files') + + createUserShareWithEdit(folderName).should(({ response }) => { + // Verify permission value is 31 (ALL with SHARE: READ=1 + UPDATE=2 + CREATE=4 + DELETE=8 + SHARE=16) + expect(response?.body?.ocs?.data?.permissions).to.equal(31) + }) + }) + + it('Creates user share with "Allow editing" with SHARE permission for files', () => { + const fileName = 'test-file-with-share.txt' + cy.uploadContent(alice, new Blob(['content']), 'text/plain', `/${fileName}`) + cy.login(alice) + cy.visit('/apps/files') + + createUserShareWithEdit(fileName).should(({ response }) => { + // Verify permission value is 19 (ALL_FILE with SHARE: READ=1 + UPDATE=2 + SHARE=16) + expect(response?.body?.ocs?.data?.permissions).to.equal(19) + }) + }) + }) + + describe('With SHARE excluded from edit (config enabled)', () => { + beforeEach(() => { + cy.runOccCommand('config:app:set --value yes files_sharing shareapi_exclude_reshare_from_edit') + }) + + it('Creates user share with "Allow editing" without SHARE permission for folders', () => { + const folderName = 'test-folder-no-share' + cy.mkdir(alice, `/${folderName}`) + cy.login(alice) + cy.visit('/apps/files') + + createUserShareWithEdit(folderName).should(({ response }) => { + // Verify permission value is 15 (ALL without SHARE: READ=1 + UPDATE=2 + CREATE=4 + DELETE=8) + expect(response?.body?.ocs?.data?.permissions).to.equal(15) + }) + }) + + it('Creates user share with "Allow editing" without SHARE permission for files', () => { + const fileName = 'test-file-no-share.txt' + cy.uploadContent(alice, new Blob(['content']), 'text/plain', `/${fileName}`) + cy.login(alice) + cy.visit('/apps/files') + + createUserShareWithEdit(fileName).should(({ response }) => { + // Verify permission value is 3 (ALL_FILE without SHARE: READ=1 + UPDATE=2) + expect(response?.body?.ocs?.data?.permissions).to.equal(3) + }) + }) + }) +})