mirror of
https://github.com/nextcloud/server.git
synced 2026-02-11 14:54:02 -05:00
Merge pull request #30862 from nextcloud/feat/crud_share_permission
Support CRUD share permissions
This commit is contained in:
commit
1bfd001cf6
11 changed files with 638 additions and 126 deletions
|
|
@ -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 (
|
||||
|
|
|
|||
291
apps/files_sharing/src/components/SharePermissionsEditor.vue
Normal file
291
apps/files_sharing/src/components/SharePermissionsEditor.vue
Normal 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>
|
||||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
123
apps/files_sharing/src/lib/SharePermissionsToolBox.js
Normal file
123
apps/files_sharing/src/lib/SharePermissionsToolBox.js
Normal 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))
|
||||
}
|
||||
96
apps/files_sharing/src/lib/SharePermissionsToolBox.spec.js
Normal file
96
apps/files_sharing/src/lib/SharePermissionsToolBox.spec.js
Normal 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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
4
dist/core-common.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-common.js.map
vendored
2
dist/core-common.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_sharing-files_sharing_tab.js
vendored
4
dist/files_sharing-files_sharing_tab.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_sharing-files_sharing_tab.js.map
vendored
2
dist/files_sharing-files_sharing_tab.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue