Merge pull request #30862 from nextcloud/feat/crud_share_permission

Support CRUD share permissions
This commit is contained in:
Louis 2022-02-16 15:31:09 +01:00 committed by GitHub
commit 1bfd001cf6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 638 additions and 126 deletions

View file

@ -1003,6 +1003,13 @@ class ShareAPIController extends OCSController {
return new DataResponse(array_values($shares));
}
/**
* Check whether a set of permissions contains the permissions to check.
*/
private function hasPermission(int $permissionsSet, int $permissionsToCheck): bool {
return ($permissionsSet & $permissionsToCheck) === $permissionsToCheck;
}
/**
* @NoAdminRequired
@ -1104,16 +1111,16 @@ class ShareAPIController extends OCSController {
$newPermissions = $newPermissions & ~Constants::PERMISSION_SHARE;
}
if ($newPermissions !== null &&
!in_array($newPermissions, [
Constants::PERMISSION_READ,
Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE, // legacy
Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE, // correct
Constants::PERMISSION_CREATE, // hidden file list
Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE, // allow to edit single files
], true)
) {
throw new OCSBadRequestException($this->l->t('Cannot change permissions for public share links'));
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 (

View file

@ -0,0 +1,291 @@
<!--
- @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
-
- @author Louis Chmn <louis@chmn.me>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<span>
<!-- file -->
<ActionCheckbox v-if="!isFolder"
:checked="shareHasPermissions(atomicPermissions.UPDATE)"
:disabled="saving"
@update:checked="toggleSharePermissions(atomicPermissions.UPDATE)">
{{ t('files_sharing', 'Allow editing') }}
</ActionCheckbox>
<!-- folder -->
<template v-if="isFolder && fileHasCreatePermission && config.isPublicUploadEnabled">
<template v-if="!showCustomPermissionsForm">
<ActionRadio :checked="sharePermissionEqual(bundledPermissions.READ_ONLY)"
:value="bundledPermissions.READ_ONLY"
:name="randomFormName"
:disabled="saving"
@change="setSharePermissions(bundledPermissions.READ_ONLY)">
{{ t('files_sharing', 'Read only') }}
</ActionRadio>
<ActionRadio :checked="sharePermissionEqual(bundledPermissions.UPLOAD_AND_UPDATE)"
:value="bundledPermissions.UPLOAD_AND_UPDATE"
:disabled="saving"
:name="randomFormName"
@change="setSharePermissions(bundledPermissions.UPLOAD_AND_UPDATE)">
{{ t('files_sharing', 'Allow upload and editing') }}
</ActionRadio>
<ActionRadio :checked="sharePermissionEqual(bundledPermissions.FILE_DROP)"
:value="bundledPermissions.FILE_DROP"
:disabled="saving"
:name="randomFormName"
class="sharing-entry__action--public-upload"
@change="setSharePermissions(bundledPermissions.FILE_DROP)">
{{ t('files_sharing', 'File drop (upload only)') }}
</ActionRadio>
<!-- custom permissions button -->
<ActionButton :title="t('files_sharing', 'Custom permissions')"
@click="showCustomPermissionsForm = true">
<template #icon>
<Tune />
</template>
{{ sharePermissionsIsBundle ? "" : sharePermissionsSummary }}
</ActionButton>
</template>
<!-- custom permissions -->
<span v-else :class="{error: !sharePermissionsSetIsValid}">
<ActionCheckbox :checked="shareHasPermissions(atomicPermissions.READ)"
:disabled="saving || !canToggleSharePermissions(atomicPermissions.READ)"
@update:checked="toggleSharePermissions(atomicPermissions.READ)">
{{ t('files_sharing', 'Read') }}
</ActionCheckbox>
<ActionCheckbox :checked="shareHasPermissions(atomicPermissions.CREATE)"
:disabled="saving || !canToggleSharePermissions(atomicPermissions.CREATE)"
@update:checked="toggleSharePermissions(atomicPermissions.CREATE)">
{{ t('files_sharing', 'Upload') }}
</ActionCheckbox>
<ActionCheckbox :checked="shareHasPermissions(atomicPermissions.UPDATE)"
:disabled="saving || !canToggleSharePermissions(atomicPermissions.UPDATE)"
@update:checked="toggleSharePermissions(atomicPermissions.UPDATE)">
{{ t('files_sharing', 'Edit') }}
</ActionCheckbox>
<ActionCheckbox :checked="shareHasPermissions(atomicPermissions.DELETE)"
:disabled="saving || !canToggleSharePermissions(atomicPermissions.DELETE)"
@update:checked="toggleSharePermissions(atomicPermissions.DELETE)">
{{ t('files_sharing', 'Delete') }}
</ActionCheckbox>
<ActionButton @click="showCustomPermissionsForm = false">
<template #icon>
<ChevronLeft />
</template>
{{ t('files_sharing', 'Bundled permissions') }}
</ActionButton>
</span>
</template>
</span>
</template>
<script>
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import ActionRadio from '@nextcloud/vue/dist/Components/ActionRadio'
import ActionCheckbox from '@nextcloud/vue/dist/Components/ActionCheckbox'
import SharesMixin from '../mixins/SharesMixin'
import {
ATOMIC_PERMISSIONS,
BUNDLED_PERMISSIONS,
hasPermissions,
permissionsSetIsValid,
togglePermissions,
canTogglePermissions,
} from '../lib/SharePermissionsToolBox'
import Tune from 'vue-material-design-icons/Tune'
import ChevronLeft from 'vue-material-design-icons/ChevronLeft'
export default {
name: 'SharePermissionsEditor',
components: {
ActionButton,
ActionCheckbox,
ActionRadio,
Tune,
ChevronLeft,
},
mixins: [SharesMixin],
data() {
return {
randomFormName: Math.random().toString(27).substring(2),
showCustomPermissionsForm: false,
atomicPermissions: ATOMIC_PERMISSIONS,
bundledPermissions: BUNDLED_PERMISSIONS,
}
},
computed: {
/**
* Return the summary of custom checked permissions.
*
* @return {string}
*/
sharePermissionsSummary() {
return Object.values(this.atomicPermissions)
.filter(permission => this.shareHasPermissions(permission))
.map(permission => {
switch (permission) {
case this.atomicPermissions.CREATE:
return this.t('files_sharing', 'Upload')
case this.atomicPermissions.READ:
return this.t('files_sharing', 'Read')
case this.atomicPermissions.UPDATE:
return this.t('files_sharing', 'Edit')
case this.atomicPermissions.DELETE:
return this.t('files_sharing', 'Delete')
default:
return ''
}
})
.join(', ')
},
/**
* Return whether the share's permission is a bundle.
*
* @return {boolean}
*/
sharePermissionsIsBundle() {
return Object.values(BUNDLED_PERMISSIONS)
.map(bundle => this.sharePermissionEqual(bundle))
.filter(isBundle => isBundle)
.length > 0
},
/**
* Return whether the share's permission is valid.
*
* @return {boolean}
*/
sharePermissionsSetIsValid() {
return permissionsSetIsValid(this.share.permissions)
},
/**
* Is the current share a folder ?
* TODO: move to a proper FileInfo model?
*
* @return {boolean}
*/
isFolder() {
return this.fileInfo.type === 'dir'
},
/**
* Does the current file/folder have create permissions.
* TODO: move to a proper FileInfo model?
*
* @return {boolean}
*/
fileHasCreatePermission() {
return !!(this.fileInfo.permissions & ATOMIC_PERMISSIONS.CREATE)
},
},
mounted() {
// Show the Custom Permissions view on open if the permissions set is not a bundle.
this.showCustomPermissionsForm = !this.sharePermissionsIsBundle
},
methods: {
/**
* Return whether the share has the exact given permissions.
*
* @param {number} permissions - the permissions to check.
*
* @return {boolean}
*/
sharePermissionEqual(permissions) {
// We use the share's permission without PERMISSION_SHARE as it is not relevant here.
return (this.share.permissions & ~ATOMIC_PERMISSIONS.SHARE) === permissions
},
/**
* Return whether the share has the given permissions.
*
* @param {number} permissions - the permissions to check.
*
* @return {boolean}
*/
shareHasPermissions(permissions) {
return hasPermissions(this.share.permissions, permissions)
},
/**
* Set the share permissions to the given permissions.
*
* @param {number} permissions - the permissions to set.
*
* @return {void}
*/
setSharePermissions(permissions) {
this.share.permissions = permissions
this.queueUpdate('permissions')
},
/**
* Return whether some given permissions can be toggled.
*
* @param {number} permissionsToToggle - the permissions to toggle.
*
* @return {boolean}
*/
canToggleSharePermissions(permissionsToToggle) {
return canTogglePermissions(this.share.permissions, permissionsToToggle)
},
/**
* Toggle a given permission.
*
* @param {number} permissions - the permissions to toggle.
*
* @return {void}
*/
toggleSharePermissions(permissions) {
this.share.permissions = togglePermissions(this.share.permissions, permissions)
if (!this.permissionsSetIsValid(this.share.permissions)) {
return
}
this.queueUpdate('permissions')
},
},
}
</script>
<style lang="scss" scoped>
.error {
::v-deep .action-checkbox__label:before {
border: 1px solid var(--color-error);
}
}
</style>

View file

@ -150,39 +150,12 @@
@submit="onLabelSubmit">
{{ t('files_sharing', 'Share label') }}
</ActionInput>
<!-- folder -->
<template v-if="isFolder && fileHasCreatePermission && config.isPublicUploadEnabled">
<ActionRadio :checked="sharePermissions === publicUploadRValue"
:value="publicUploadRValue"
:name="randomId"
:disabled="saving"
@change="togglePermissions">
{{ t('files_sharing', 'Read only') }}
</ActionRadio>
<ActionRadio :checked="sharePermissions === publicUploadRWValue"
:value="publicUploadRWValue"
:disabled="saving"
:name="randomId"
@change="togglePermissions">
{{ t('files_sharing', 'Allow upload and editing') }}
</ActionRadio>
<ActionRadio :checked="sharePermissions === publicUploadWValue"
:value="publicUploadWValue"
:disabled="saving"
:name="randomId"
class="sharing-entry__action--public-upload"
@change="togglePermissions">
{{ t('files_sharing', 'File drop (upload only)') }}
</ActionRadio>
</template>
<!-- file -->
<ActionCheckbox v-if="!isFolder"
:checked.sync="canUpdate"
:disabled="saving"
@change="queueUpdate('permissions')">
{{ t('files_sharing', 'Allow editing') }}
</ActionCheckbox>
<SharePermissionsEditor :can-reshare="canReshare"
:share.sync="share"
:file-info="fileInfo" />
<ActionSeparator />
<ActionCheckbox :checked.sync="share.hideDownload"
:disabled="saving"
@ -282,6 +255,8 @@
@submit="onNoteSubmit" />
</template>
<ActionSeparator />
<!-- external actions -->
<ExternalShareAction v-for="action in externalLinkActions"
:id="action.id"
@ -336,14 +311,15 @@ import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import ActionCheckbox from '@nextcloud/vue/dist/Components/ActionCheckbox'
import ActionInput from '@nextcloud/vue/dist/Components/ActionInput'
import ActionLink from '@nextcloud/vue/dist/Components/ActionLink'
import ActionRadio from '@nextcloud/vue/dist/Components/ActionRadio'
import ActionText from '@nextcloud/vue/dist/Components/ActionText'
import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator'
import ActionTextEditable from '@nextcloud/vue/dist/Components/ActionTextEditable'
import Actions from '@nextcloud/vue/dist/Components/Actions'
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip'
import ExternalShareAction from './ExternalShareAction'
import SharePermissionsEditor from './SharePermissionsEditor'
import GeneratePassword from '../utils/GeneratePassword'
import Share from '../models/Share'
import SharesMixin from '../mixins/SharesMixin'
@ -355,13 +331,14 @@ export default {
Actions,
ActionButton,
ActionCheckbox,
ActionRadio,
ActionInput,
ActionLink,
ActionText,
ActionTextEditable,
ActionSeparator,
Avatar,
ExternalShareAction,
SharePermissionsEditor,
},
directives: {
@ -385,37 +362,12 @@ export default {
// Are we waiting for password/expiration date
pending: false,
publicUploadRWValue: OC.PERMISSION_UPDATE | OC.PERMISSION_CREATE | OC.PERMISSION_READ | OC.PERMISSION_DELETE,
publicUploadRValue: OC.PERMISSION_READ,
publicUploadWValue: OC.PERMISSION_CREATE,
ExternalLegacyLinkActions: OCA.Sharing.ExternalLinkActions.state,
ExternalShareActions: OCA.Sharing.ExternalShareActions.state,
}
},
computed: {
/**
* Return the current share permissions
* We always ignore the SHARE permission as this is used for the
* federated sharing.
*
* @return {number}
*/
sharePermissions() {
return this.share.permissions & ~OC.PERMISSION_SHARE
},
/**
* Generate a unique random id for this SharingEntryLink only
* This allows ActionRadios to have the same name prop
* but not to impact others SharingEntryLink
*
* @return {string}
*/
randomId() {
return Math.random().toString(27).substr(2)
},
/**
* Link share label
*
@ -580,48 +532,12 @@ export default {
return this.config.isDefaultExpireDateEnforced && this.share && !this.share.id
},
/**
* Can the recipient edit the file ?
*
* @return {boolean}
*/
canUpdate: {
get() {
return this.share.hasUpdatePermission
},
set(enabled) {
this.share.permissions = enabled
? OC.PERMISSION_READ | OC.PERMISSION_UPDATE
: OC.PERMISSION_READ
},
},
// if newPassword exists, but is empty, it means
// the user deleted the original password
hasUnsavedPassword() {
return this.share.newPassword !== undefined
},
/**
* Is the current share a folder ?
* TODO: move to a proper FileInfo model?
*
* @return {boolean}
*/
isFolder() {
return this.fileInfo.type === 'dir'
},
/**
* Does the current file/folder have create permissions
* TODO: move to a proper FileInfo model?
*
* @return {boolean}
*/
fileHasCreatePermission() {
return !!(this.fileInfo.permissions & OC.PERMISSION_CREATE)
},
/**
* Return the public share link
*
@ -809,17 +725,6 @@ export default {
}
},
/**
* On permissions change
*
* @param {Event} event js event
*/
togglePermissions(event) {
const permissions = parseInt(event.target.value, 10)
this.share.permissions = permissions
this.queueUpdate('permissions')
},
/**
* Label changed, let's save it to a different key
*

View file

@ -0,0 +1,123 @@
/**
* @copyright 2022 Louis Chmn <louis@chmn.me>
*
* @author Louis Chmn <louis@chmn.me>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
export const ATOMIC_PERMISSIONS = {
NONE: 0,
READ: 1,
UPDATE: 2,
CREATE: 4,
DELETE: 8,
SHARE: 16,
}
export 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 | ATOMIC_PERMISSIONS.SHARE,
}
/**
* Return whether a given permissions set contains some permissions.
*
* @param {number} initialPermissionSet - the permissions set.
* @param {number} permissionsToCheck - the permissions to check.
* @return {boolean}
*/
export function hasPermissions(initialPermissionSet, permissionsToCheck) {
return initialPermissionSet !== ATOMIC_PERMISSIONS.NONE && (initialPermissionSet & permissionsToCheck) === permissionsToCheck
}
/**
* Return whether a given permissions set is valid.
*
* @param {number} permissionsSet - the permissions set.
*
* @return {boolean}
*/
export function permissionsSetIsValid(permissionsSet) {
// Must have at least READ or CREATE permission.
if (!hasPermissions(permissionsSet, ATOMIC_PERMISSIONS.READ) && !hasPermissions(permissionsSet, ATOMIC_PERMISSIONS.CREATE)) {
return false
}
// Must have READ permission if have UPDATE or DELETE.
if (!hasPermissions(permissionsSet, ATOMIC_PERMISSIONS.READ) && (
hasPermissions(permissionsSet, ATOMIC_PERMISSIONS.UPDATE) || hasPermissions(permissionsSet, ATOMIC_PERMISSIONS.DELETE)
)) {
return false
}
return true
}
/**
* Add some permissions to an initial set of permissions.
*
* @param {number} initialPermissionSet - the initial permissions.
* @param {number} permissionsToAdd - the permissions to add.
*
* @return {number}
*/
export function addPermissions(initialPermissionSet, permissionsToAdd) {
return initialPermissionSet | permissionsToAdd
}
/**
* Remove some permissions from an initial set of permissions.
*
* @param {number} initialPermissionSet - the initial permissions.
* @param {number} permissionsToSubtract - the permissions to remove.
*
* @return {number}
*/
export function subtractPermissions(initialPermissionSet, permissionsToSubtract) {
return initialPermissionSet & ~permissionsToSubtract
}
/**
* Toggle some permissions from an initial set of permissions.
*
* @param {number} initialPermissionSet - the permissions set.
* @param {number} permissionsToToggle - the permissions to toggle.
*
* @return {number}
*/
export function togglePermissions(initialPermissionSet, permissionsToToggle) {
if (hasPermissions(initialPermissionSet, permissionsToToggle)) {
return subtractPermissions(initialPermissionSet, permissionsToToggle)
} else {
return addPermissions(initialPermissionSet, permissionsToToggle)
}
}
/**
* Return whether some given permissions can be toggled from a permission set.
*
* @param {number} permissionSet - the initial permissions set.
* @param {number} permissionsToToggle - the permissions to toggle.
*
* @return {boolean}
*/
export function canTogglePermissions(permissionSet, permissionsToToggle) {
return permissionsSetIsValid(togglePermissions(permissionSet, permissionsToToggle))
}

View file

@ -0,0 +1,96 @@
/**
* @copyright 2022 Louis Chmn <louis@chmn.me>
*
* @author Louis Chmn <louis@chmn.me>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import {
ATOMIC_PERMISSIONS,
BUNDLED_PERMISSIONS,
addPermissions,
subtractPermissions,
hasPermissions,
permissionsSetIsValid,
togglePermissions,
canTogglePermissions,
} from '../lib/SharePermissionsToolBox'
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 | 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)
})
test('Subtract permissions', () => {
expect(subtractPermissions(ATOMIC_PERMISSIONS.READ, ATOMIC_PERMISSIONS.NONE)).toBe(ATOMIC_PERMISSIONS.READ)
expect(subtractPermissions(ATOMIC_PERMISSIONS.READ, ATOMIC_PERMISSIONS.READ)).toBe(ATOMIC_PERMISSIONS.NONE)
expect(subtractPermissions(ATOMIC_PERMISSIONS.READ, ATOMIC_PERMISSIONS.UPDATE)).toBe(ATOMIC_PERMISSIONS.READ)
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 | ATOMIC_PERMISSIONS.SHARE)
})
test('Has permissions', () => {
expect(hasPermissions(ATOMIC_PERMISSIONS.NONE, ATOMIC_PERMISSIONS.READ)).toBe(false)
expect(hasPermissions(ATOMIC_PERMISSIONS.READ, ATOMIC_PERMISSIONS.NONE)).toBe(true)
expect(hasPermissions(BUNDLED_PERMISSIONS.READ_ONLY, ATOMIC_PERMISSIONS.READ)).toBe(true)
expect(hasPermissions(BUNDLED_PERMISSIONS.READ_ONLY, ATOMIC_PERMISSIONS.UPDATE)).toBe(false)
expect(hasPermissions(BUNDLED_PERMISSIONS.READ_ONLY, ATOMIC_PERMISSIONS.DELETE)).toBe(false)
expect(hasPermissions(BUNDLED_PERMISSIONS.ALL, ATOMIC_PERMISSIONS.DELETE)).toBe(true)
})
test('Toggle permissions', () => {
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)
})
test('Permissions set is valid', () => {
expect(permissionsSetIsValid(ATOMIC_PERMISSIONS.NONE)).toBe(false)
expect(permissionsSetIsValid(ATOMIC_PERMISSIONS.READ)).toBe(true)
expect(permissionsSetIsValid(ATOMIC_PERMISSIONS.CREATE)).toBe(true)
expect(permissionsSetIsValid(ATOMIC_PERMISSIONS.UPDATE)).toBe(false)
expect(permissionsSetIsValid(ATOMIC_PERMISSIONS.DELETE)).toBe(false)
expect(permissionsSetIsValid(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE)).toBe(true)
expect(permissionsSetIsValid(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.DELETE)).toBe(true)
expect(permissionsSetIsValid(ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.UPDATE)).toBe(false)
expect(permissionsSetIsValid(ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.DELETE)).toBe(false)
expect(permissionsSetIsValid(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.UPDATE)).toBe(true)
expect(permissionsSetIsValid(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.DELETE)).toBe(true)
})
test('Toggle permissions', () => {
expect(canTogglePermissions(ATOMIC_PERMISSIONS.READ, ATOMIC_PERMISSIONS.READ)).toBe(false)
expect(canTogglePermissions(ATOMIC_PERMISSIONS.CREATE, ATOMIC_PERMISSIONS.READ)).toBe(true)
expect(canTogglePermissions(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.UPDATE, ATOMIC_PERMISSIONS.READ)).toBe(false)
expect(canTogglePermissions(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.DELETE, ATOMIC_PERMISSIONS.READ)).toBe(false)
expect(canTogglePermissions(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.UPDATE, ATOMIC_PERMISSIONS.READ)).toBe(false)
expect(canTogglePermissions(ATOMIC_PERMISSIONS.READ | ATOMIC_PERMISSIONS.CREATE | ATOMIC_PERMISSIONS.DELETE, ATOMIC_PERMISSIONS.READ)).toBe(false)
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)
})
})

View file

@ -31,9 +31,6 @@ import axios from '@nextcloud/axios'
import Share from '../models/Share'
const shareUrl = generateOcsUrl('apps/files_sharing/api/v1/shares')
const headers = {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
}
export default {
methods: {
@ -103,7 +100,7 @@ export default {
*/
async updateShare(id, properties) {
try {
const request = await axios.put(shareUrl + `/${id}`, properties, headers)
const request = await axios.put(shareUrl + `/${id}`, properties)
if (!request?.data?.ocs) {
throw request
}

View file

@ -2793,6 +2793,99 @@ class ShareAPIControllerTest extends TestCase {
}
public function publicLinkValidPermissionsProvider() {
return [
[\OCP\Constants::PERMISSION_CREATE],
[\OCP\Constants::PERMISSION_READ],
[\OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_UPDATE],
[\OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_DELETE],
[\OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_CREATE],
];
}
/**
* @dataProvider publicLinkValidPermissionsProvider
*/
public function testUpdateLinkShareSetCRUDPermissions($permissions) {
$ocs = $this->mockFormatShare();
$folder = $this->getMockBuilder(Folder::class)->getMock();
$folder->method('getId')
->willReturn(42);
$share = \OC::$server->getShareManager()->newShare();
$share->setPermissions(\OCP\Constants::PERMISSION_ALL)
->setSharedBy($this->currentUser)
->setShareType(IShare::TYPE_LINK)
->setPassword('password')
->setNode($folder);
$this->shareManager->method('getShareById')->with('ocinternal:42')->willReturn($share);
$this->shareManager->method('shareApiLinkAllowPublicUpload')->willReturn(true);
$this->shareManager->method('getSharedWith')->willReturn([]);
$this->shareManager
->expects($this->any())
->method('updateShare')
->willReturnArgument(0);
$userFolder = $this->createMock(Folder::class);
$this->rootFolder->method('getUserFolder')
->with($this->currentUser)
->willReturn($userFolder);
$userFolder->method('getById')
->with(42)
->willReturn([$folder]);
$mountPoint = $this->createMock(IMountPoint::class);
$folder->method('getMountPoint')
->willReturn($mountPoint);
$mountPoint->method('getStorageRootId')
->willReturn(42);
$expected = new DataResponse([]);
$result = $ocs->updateShare(42, $permissions, 'password', null, 'true', null);
$this->assertInstanceOf(get_class($expected), $result);
$this->assertEquals($expected->getData(), $result->getData());
}
public function publicLinkInvalidPermissionsProvider1() {
return [
[\OCP\Constants::PERMISSION_DELETE],
[\OCP\Constants::PERMISSION_UPDATE],
[\OCP\Constants::PERMISSION_SHARE],
];
}
/**
* @dataProvider publicLinkInvalidPermissionsProvider1
*/
public function testUpdateLinkShareSetInvalidCRUDPermissions1($permissions) {
$this->expectException(\OCP\AppFramework\OCS\OCSBadRequestException::class);
$this->expectExceptionMessage('Share must at least have READ or CREATE permissions');
$this->testUpdateLinkShareSetCRUDPermissions($permissions);
}
public function publicLinkInvalidPermissionsProvider2() {
return [
[\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_DELETE],
[\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE],
];
}
/**
* @dataProvider publicLinkInvalidPermissionsProvider2
*/
public function testUpdateLinkShareSetInvalidCRUDPermissions2($permissions) {
$this->expectException(\OCP\AppFramework\OCS\OCSBadRequestException::class);
$this->expectExceptionMessage('Share must have READ permission if UPDATE or DELETE permission is set.');
$this->testUpdateLinkShareSetCRUDPermissions($permissions);
}
public function testUpdateLinkShareInvalidDate() {
$this->expectException(\OCP\AppFramework\OCS\OCSBadRequestException::class);
$this->expectExceptionMessage('Invalid date. Format must be YYYY-MM-DD');

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long