diff --git a/changelog/unreleased/enhancement-deny-subfolder-share b/changelog/unreleased/enhancement-deny-subfolder-share new file mode 100644 index 00000000000..48b3628c419 --- /dev/null +++ b/changelog/unreleased/enhancement-deny-subfolder-share @@ -0,0 +1,7 @@ +Enhancement: Deny subfolders inside share + +Sub-folders within user- and group-shares can now be denied for certain share receivers if the backend is capable of negative ACLs. +Please note that the state of this feature is experimental and needs to be enabled in the backend. + +https://github.com/owncloud/web/pull/7190 +https://github.com/owncloud/web/issues/7180 diff --git a/packages/web-app-files/src/components/SideBar/Shares/Collaborators/RoleDropdown.vue b/packages/web-app-files/src/components/SideBar/Shares/Collaborators/RoleDropdown.vue index 469f56c9710..58b5672ee38 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/Collaborators/RoleDropdown.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/Collaborators/RoleDropdown.vue @@ -170,6 +170,8 @@ export default defineComponent({ inviteLabel() { if (this.selectedRole.hasCustomPermissions) { return this.$gettext('Invite with custom permissions') + } else if (this.selectedRole.permissions().includes(SharePermissions.denied)) { + return this.$gettext('Deny access') } else { return this.$gettextInterpolate(this.$gettext('Invite as %{ name }'), { name: this.$gettext(this.selectedRole.inlineLabel) || '' @@ -199,7 +201,11 @@ export default defineComponent({ ) } - return PeopleShareRoles.list(this.resource.isFolder, this.allowCustomSharing !== false) + return PeopleShareRoles.list( + this.resource.isFolder, + this.allowCustomSharing !== false, + this.resource.canDeny() + ) }, availablePermissions() { if (this.incomingParentShare.value && this.resourceIsSharable) { @@ -234,7 +240,10 @@ export default defineComponent({ } else if (this.resourceIsSpace) { this.selectedRole = SpacePeopleShareRoles.list()[0] } else { - this.selectedRole = PeopleShareRoles.list(this.resource.isFolder)[0] + this.selectedRole = PeopleShareRoles.list( + this.resource.isFolder, + this.resource.canDeny() + )[0] } if (this.selectedRole.hasCustomPermissions) { diff --git a/packages/web-app-files/src/helpers/resources.ts b/packages/web-app-files/src/helpers/resources.ts index 9a80d19b48f..b8329d13698 100644 --- a/packages/web-app-files/src/helpers/resources.ts +++ b/packages/web-app-files/src/helpers/resources.ts @@ -107,6 +107,9 @@ export function buildResource(resource): Resource { isReceivedShare: function () { return this.permissions.indexOf(DavPermission.Shared) >= 0 }, + canDeny: function () { + return this.permissions.indexOf(DavPermission.Deny) >= 0 + }, getDomSelector: () => extractDomSelector(id) } } @@ -276,6 +279,7 @@ export function buildSharedResource( resource.canUpload = () => SharePermissions.create.enabled(share.permissions) resource.isMounted = () => false resource.share = buildShare(share, resource, allowSharePermission) + resource.canDeny = () => SharePermissions.denied.enabled(share.permissions) resource.getDomSelector = () => extractDomSelector(share.id) return resource diff --git a/packages/web-app-files/tests/unit/components/SideBar/Shares/Collaborators/RoleDropdown.spec.js b/packages/web-app-files/tests/unit/components/SideBar/Shares/Collaborators/RoleDropdown.spec.js index 3f03ba98f47..10df203f74c 100644 --- a/packages/web-app-files/tests/unit/components/SideBar/Shares/Collaborators/RoleDropdown.spec.js +++ b/packages/web-app-files/tests/unit/components/SideBar/Shares/Collaborators/RoleDropdown.spec.js @@ -305,6 +305,7 @@ function getResource({ shareTypes: [], downloadURL: '', isReceivedShare: () => isReceivedShare, - canShare: () => true + canShare: () => true, + canDeny: () => false } } diff --git a/packages/web-app-files/tests/unit/components/SideBar/Shares/FileShares.spec.js b/packages/web-app-files/tests/unit/components/SideBar/Shares/FileShares.spec.js index c8a5a54eea7..ba632351115 100644 --- a/packages/web-app-files/tests/unit/components/SideBar/Shares/FileShares.spec.js +++ b/packages/web-app-files/tests/unit/components/SideBar/Shares/FileShares.spec.js @@ -173,7 +173,8 @@ function getResource({ isReceivedShare: () => true, canBeDeleted: () => true, canRename: () => true, - canShare: () => canShare + canShare: () => canShare, + canDeny: () => false } } diff --git a/packages/web-client/src/helpers/resource/types.ts b/packages/web-client/src/helpers/resource/types.ts index 3e0d80f0862..8717024786b 100644 --- a/packages/web-client/src/helpers/resource/types.ts +++ b/packages/web-client/src/helpers/resource/types.ts @@ -34,6 +34,7 @@ export interface Resource { canRename?(): boolean canBeDeleted?(): boolean canBeRestored?(): boolean + canDeny?(): boolean isReceivedShare?(): boolean isMounted?(): boolean diff --git a/packages/web-client/src/helpers/share/permission.ts b/packages/web-client/src/helpers/share/permission.ts index 71332cfb58d..0f0cca35958 100644 --- a/packages/web-client/src/helpers/share/permission.ts +++ b/packages/web-client/src/helpers/share/permission.ts @@ -61,6 +61,8 @@ export abstract class SharePermissions { static readonly share = new SharePermission('share', SharePermissionBit.Share, $gettext('Share')) + static readonly denied = new SharePermission('denied', 64, $gettext('Deny')) + static permissionsToBitmask(permissions: SharePermission[]): number { return (permissions || []).reduce((b: number, p: SharePermission) => b | p.bit, 0) } diff --git a/packages/web-client/src/helpers/share/role.ts b/packages/web-client/src/helpers/share/role.ts index 875e7c64383..eacb954f069 100644 --- a/packages/web-client/src/helpers/share/role.ts +++ b/packages/web-client/src/helpers/share/role.ts @@ -185,6 +185,14 @@ export const peopleRoleCustomFolder = new CustomShareRole( SharePermissions.share ] ) +export const peopleRoleDenyFolder = new PeopleShareRole( + 'denied', + true, + $gettext('No access'), + $gettext('no access'), + 'user-unfollow', + [SharePermissions.denied] +) export const linkRoleInternalFile = new LinkShareRole( 'none', false, @@ -288,7 +296,8 @@ export abstract class SpacePeopleShareRoles { } static getByBitmask(bitmask: number): ShareRole { - return this.all.find((r) => r.bitmask(true) === bitmask) + return this.all // Retrieve all possible options always, even if deny is not enabled + .find((r) => r.bitmask(true) === bitmask) } } @@ -302,8 +311,11 @@ export abstract class PeopleShareRoles { static readonly allWithCustom = [...this.all, peopleRoleCustomFile, peopleRoleCustomFolder] - static list(isFolder: boolean, hasCustom = true): ShareRole[] { - return (hasCustom ? this.allWithCustom : this.all).filter((r) => r.folder === isFolder) + static list(isFolder: boolean, hasCustom = true, canDeny = false): ShareRole[] { + return [ + ...(hasCustom ? this.allWithCustom : this.all), + ...(canDeny ? [peopleRoleDenyFolder] : []) + ].filter((r) => r.folder === isFolder) } static custom(isFolder: boolean): ShareRole { @@ -311,7 +323,7 @@ export abstract class PeopleShareRoles { } static getByBitmask(bitmask: number, isFolder: boolean, allowSharing: boolean): ShareRole { - const role = this.allWithCustom + const role = [...this.allWithCustom, peopleRoleDenyFolder] // Retrieve all possible options always, even if deny is not enabled .filter((r) => !r.hasCustomPermissions) .find((r) => r.folder === isFolder && r.bitmask(allowSharing) === bitmask) return role || this.custom(isFolder) @@ -401,7 +413,8 @@ const shareRoleDescriptions = { [peopleRoleEditorFolder.bitmask(false)]: $gettext('Upload, edit, delete, download and preview'), [peopleRoleEditorFolder.bitmask(true)]: $gettext( 'Upload, edit, delete, download, preview and share' - ) + ), + [peopleRoleDenyFolder.bitmask(false)]: $gettext('Deny access') } /** diff --git a/packages/web-client/src/helpers/space/functions.ts b/packages/web-client/src/helpers/space/functions.ts index fe6405c4fb8..9d1fbbb9ed3 100644 --- a/packages/web-client/src/helpers/space/functions.ts +++ b/packages/web-client/src/helpers/space/functions.ts @@ -123,6 +123,7 @@ export function buildSpace(space) { isReceivedShare: function () { return false }, + canDeny: () => false, getDomSelector: () => extractDomSelector(space.id) } } diff --git a/packages/web-pkg/src/constants/dav.ts b/packages/web-pkg/src/constants/dav.ts index 11c7dd10f10..718211d230c 100644 --- a/packages/web-pkg/src/constants/dav.ts +++ b/packages/web-pkg/src/constants/dav.ts @@ -8,6 +8,7 @@ export abstract class DavPermission { static readonly Updateable: string = 'NV' static readonly FileUpdateable: string = 'W' static readonly FolderCreateable: string = 'CK' + static readonly Deny: string = 'Z' } export abstract class DavProperty {