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

[full-ci] Fix inheritance of share permissions on reshares #7015

Merged
merged 10 commits into from
May 23, 2022
6 changes: 6 additions & 0 deletions changelog/unreleased/bugfix-share-permissions-inheritance
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Bugfix: Inheritance of share permissions

We've fixed a bug where the permissions of a share were not inherited when trying to reshare a resource. We've also disabled the role-select-dropdown if only one role is available for sharing.

https://github.com/owncloud/web/pull/7015
https://github.com/owncloud/web/issues/2963
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<template>
<span v-if="selectedRole" class="oc-flex oc-flex-middle">
<span v-if="availableRoles.length === 1">
<span v-if="!existingRole" v-text="inviteLabel" />
<span v-else>{{ $gettext(selectedRole.label) }}</span>
</span>
<oc-button
v-else
:id="roleButtonId"
class="files-recipient-role-select-btn"
appearance="raw"
Expand All @@ -11,6 +16,7 @@
<oc-icon name="arrow-down-s" />
</oc-button>
<oc-drop
v-if="availableRoles.length > 1"
ref="rolesDrop"
:toggle="'#' + roleButtonId"
mode="click"
Expand All @@ -35,6 +41,7 @@
</oc-list>
</oc-drop>
<oc-drop
v-if="availableRoles.length > 1"
ref="customPermissionsDrop"
class="files-recipient-custom-permissions-drop"
mode="manual"
Expand Down Expand Up @@ -75,13 +82,14 @@
</template>

<script>
import { mapGetters } from 'vuex'
import { mapGetters, mapState } from 'vuex'
import get from 'lodash-es/get'
import RoleItem from '../Shared/RoleItem.vue'
import {
PeopleShareRoles,
SharePermissions,
ShareRole,
ShareTypes,
SpacePeopleShareRoles
} from '../../../../helpers/share'
import * as uuid from 'uuid'
Expand Down Expand Up @@ -121,6 +129,7 @@ export default {
}
},
computed: {
...mapState('Files', ['sharesTree']),
...mapGetters(['capabilities']),

roleButtonId() {
Expand All @@ -144,16 +153,39 @@ export default {
customPermissionsRole() {
return PeopleShareRoles.custom(this.resource.isFolder)
},
resourceIsSharable() {
return this.allowSharePermission && this.resource.canShare()
},
share() {
const userShares = this.sharesTree[this.resource.path]?.filter((s) =>
ShareTypes.containsAnyValue(ShareTypes.individuals, [s.shareType])
)

return userShares?.length ? userShares[0] : undefined
},
allowCustomSharing() {
return this.capabilities?.files_sharing?.allow_custom
},
availableRoles() {
if (this.resourceIsSpace) {
return SpacePeopleShareRoles.list()
}
return PeopleShareRoles.list(
this.resource.isFolder,
this.capabilities?.files_sharing?.allow_custom !== false
)

if (this.resource.isReceivedShare() && this.resourceIsSharable && this.share) {
return PeopleShareRoles.filterByBitmask(
parseInt(this.share.permissions),
this.resource.isFolder,
this.allowSharePermission,
this.allowCustomSharing !== false
)
}

return PeopleShareRoles.list(this.resource.isFolder, this.allowCustomSharing !== false)
},
availablePermissions() {
if (this.resource.isReceivedShare() && this.resourceIsSharable && this.share) {
return SharePermissions.bitmaskToPermissions(parseInt(this.share.permissions))
}
return this.customPermissionsRole.permissions(this.allowSharePermission)
},
resourceIsSpace() {
Expand Down
25 changes: 25 additions & 0 deletions packages/web-app-files/src/helpers/share/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,31 @@ export abstract class PeopleShareRoles {
.find((r) => r.folder === isFolder && r.bitmask(allowSharing) === bitmask)
return role || this.custom(isFolder)
}

/**
* Filter all roles that have either exactly the permissions from the bitmask or a subset of them.
* @param bitmask
* @param isFolder
* @param allowSharing
* @param allowCustom
*/
static filterByBitmask(
bitmask: number,
isFolder: boolean,
allowSharing: boolean,
allowCustom: boolean
): ShareRole[] {
const roles = this.all.filter((r) => {
return r.folder === isFolder && bitmask === (bitmask | r.bitmask(allowSharing))
})

if (allowCustom) {
const customRoles = [peopleRoleCustomFile, peopleRoleCustomFolder]
return [...roles, ...customRoles.filter((c) => c.folder === isFolder)]
}

return roles
}
}

export abstract class LinkShareRoles {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import GetTextPlugin from 'vue-gettext'
import Vuex from 'vuex'
import DesignSystem from 'owncloud-design-system'
import { PeopleShareRoles } from '@files/src/helpers/share'
import { SharePermissions, ShareTypes } from '../../../../../../src/helpers/share'

const localVue = createLocalVue()
localVue.use(DesignSystem)
Expand All @@ -25,13 +26,23 @@ const stubs = {
'oc-icon': true
}

const store = new Vuex.Store({
getters: {
capabilities: () => {
return {}
const getStore = (sharesTree) => {
return new Vuex.Store({
getters: {
capabilities: () => {
return {}
}
},
modules: {
Files: {
namespaced: true,
state: {
sharesTree
}
}
}
}
})
})
}

// needs differentiation between file and folder type?

Expand Down Expand Up @@ -125,6 +136,30 @@ describe('RoleDropdown', () => {
// })
it.todo('emits an event upon role selection')
})
describe('custom permissions', () => {
it.each([
SharePermissions.read.bit + SharePermissions.share.bit,
SharePermissions.read.bit +
SharePermissions.share.bit +
SharePermissions.update.bit +
SharePermissions.create.bit,
SharePermissions.read.bit + SharePermissions.share.bit + SharePermissions.delete.bit
])("inherits the parents share's permissions: %s", (sharePermissions) => {
const wrapper = getShallowMountedWrapper({
existingRole: PeopleShareRoles.list(true)[0],
isReceivedShare: true,
sharesTree: {
'/testfolder': [
{
permissions: sharePermissions,
shareType: ShareTypes.user.value
}
]
}
})
expect(wrapper).toMatchSnapshot()
})
})
describe('when an existing role is present', () => {
it.each(['folder', 'file'])(
'renders a button with existing role if given for resource type %s',
Expand All @@ -135,6 +170,21 @@ describe('RoleDropdown', () => {
expect(wrapper).toMatchSnapshot()
}
)
it('does not render a button if only one role is available', () => {
const wrapper = getShallowMountedWrapper({
existingRole: PeopleShareRoles.list(true)[0],
isReceivedShare: true,
sharesTree: {
'/testfolder': [
{
permissions: SharePermissions.read.bit,
shareType: ShareTypes.user.value
}
]
}
})
expect(wrapper).toMatchSnapshot()
})
it.todo(
'displays a dropdown with viewer, editor and custom permissions if no custom permissions had been selected'
)
Expand Down Expand Up @@ -204,25 +254,37 @@ function getShallowMountedWrapper(data) {
return shallowMount(RoleDropdown, getMountOptions(data))
}

function getMountOptions({ existingRole, shareId, resourceType = 'folder' }) {
function getMountOptions({
existingRole,
shareId,
resourceType = 'folder',
sharesTree = {},
isReceivedShare = false
}) {
return {
propsData: {
resource: getResource({
filename: resourceType === 'folder' ? 'testfolder' : 'testfile',
extension: resourceType === 'folder' ? '' : 'jpg',
type: resourceType
type: resourceType,
isReceivedShare
}),
existingRole,
shareId,
allowSharePermission: true
},
store,
store: getStore(sharesTree),
localVue,
stubs
}
}

function getResource({ filename = 'testFile', extension = 'txt', type = 'file' }) {
function getResource({
filename = 'testFile',
extension = 'txt',
type = 'file',
isReceivedShare = false
}) {
return {
id: '4',
fileId: '4',
Expand All @@ -239,6 +301,8 @@ function getResource({ filename = 'testFile', extension = 'txt', type = 'file' }
starred: false,
etag: '"89128c0e8122002db57bd19c9ec33004"',
shareTypes: [],
downloadURL: ''
downloadURL: '',
isReceivedShare: () => isReceivedShare,
canShare: () => true
}
}
Loading