Skip to content

Commit

Permalink
Merge pull request #10038 from owncloud/link-default-permissions-capa…
Browse files Browse the repository at this point in the history
…bility

feat: respect default link permissions
  • Loading branch information
kulmann authored Nov 27, 2023
2 parents 4a242e9 + 065234e commit 1867db1
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 93 deletions.
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-default-link-permissions
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Default link permission

When creating a new link, Web now respects the default permissions coming from the server.

https://github.com/owncloud/web/pull/10037
https://github.com/owncloud/web/issues/9919
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import { computed } from 'vue'
import {
createQuicklink,
getDefaultLinkPermissions,
showQuickLinkPasswordModal,
useAbility,
useClientService,
Expand All @@ -37,6 +38,7 @@ import {
} from '@ownclouders/web-pkg'
import { Resource } from '@ownclouders/web-client'
import { useGettext } from 'vue3-gettext'
import { SharePermissionBit } from '@ownclouders/web-client/src/helpers'
export default {
setup() {
Expand Down Expand Up @@ -88,7 +90,9 @@ export default {
store.getters.capabilities?.files_sharing?.public?.password?.enforced_for?.read_only ===
true
if (passwordEnforced) {
const permissions = getDefaultLinkPermissions({ ability, store })
if (passwordEnforced && permissions > SharePermissionBit.Internal) {
showQuickLinkPasswordModal(
{ store, $gettext: language.$gettext, passwordPolicyService },
async (password) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ import {
useCapabilityFilesSharingPublicCanContribute,
useCapabilityFilesSharingPublicAlias,
useAbility,
usePasswordPolicyService
usePasswordPolicyService,
getDefaultLinkPermissions
} from '@ownclouders/web-pkg'
import { shareViaLinkHelp, shareViaIndirectLinkHelp } from '../../../helpers/contextualHelpers'
import {
Expand Down Expand Up @@ -219,6 +220,7 @@ export default defineComponent({
return {
$store: store,
ability,
space,
resource,
incomingParentShare: inject<Share>('incomingParentShare'),
Expand Down Expand Up @@ -417,7 +419,10 @@ export default defineComponent({
this.checkLinkToCreate({
link: {
name: this.$gettext('Link'),
permissions: this.canCreatePublicLinks ? 1 : 0,
permissions: getDefaultLinkPermissions({
ability: this.ability,
store: this.$store
}).toString(),
expiration: this.expirationDate.default,
password: false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,9 @@
</template>

<script lang="ts">
import { computed, defineComponent, inject, unref } from 'vue'
import {
useAbility,
useCapabilityFilesSharingPublicAlias,
useCapabilityFilesSharingPublicCanContribute,
useCapabilityFilesSharingPublicCanEdit,
useCapabilityFilesSharingQuickLinkDefaultRole,
useCapabilityFilesSharingResharing
} from '@ownclouders/web-pkg'
import { Resource } from '@ownclouders/web-client/src'
import { defineComponent } from 'vue'
import { useAbility, getDefaultLinkPermissions, useStore } from '@ownclouders/web-pkg'
import { useGettext } from 'vue3-gettext'
import {
LinkShareRoles,
linkRoleInternalFolder,
linkRoleViewerFolder
} from '@ownclouders/web-client/src/helpers/share'
export default defineComponent({
name: 'CreateQuickLink',
Expand All @@ -54,32 +41,15 @@ export default defineComponent({
},
emits: ['createPublicLink'],
setup(props, { emit }) {
const { can } = useAbility()
const store = useStore()
const ability = useAbility()
const { $gettext } = useGettext()
const canCreatePublicLinks = computed(() => can('create-all', 'PublicLink'))
const resource = inject<Resource>('resource')
const allowResharing = useCapabilityFilesSharingResharing()
const canEdit = useCapabilityFilesSharingPublicCanEdit()
const canContribute = useCapabilityFilesSharingPublicCanContribute()
const alias = useCapabilityFilesSharingPublicAlias()
const capabilitiesRoleName = useCapabilityFilesSharingQuickLinkDefaultRole()
const createQuickLink = () => {
const roleName = !unref(canCreatePublicLinks)
? linkRoleInternalFolder.name
: unref(capabilitiesRoleName) || linkRoleViewerFolder.name
const emitData = {
link: {
name: $gettext('Link'),
permissions: LinkShareRoles.getByName(
roleName,
unref(resource).isFolder,
unref(canEdit),
unref(canContribute),
unref(alias)
)
.bitmask(unref(allowResharing))
.toString(),
permissions: getDefaultLinkPermissions({ ability, store }).toString(),
expiration: props.expirationDate.enforced ? props.expirationDate.default : null,
quicklink: true,
password: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import {
shallowMount
} from 'web-test-helpers'
import EmbedActions from 'web-app-files/src/components/EmbedActions/EmbedActions.vue'
import { getDefaultLinkPermissions } from '@ownclouders/web-pkg'
import { SharePermissionBit } from '@ownclouders/web-client/src/helpers'

jest.mock('@ownclouders/web-pkg', () => ({
...jest.requireActual('@ownclouders/web-pkg'),
createQuicklink: jest.fn().mockImplementation(({ resource, password }) => ({
url: (password ? password + '-' : '') + 'link-' + resource.id
})),
showQuickLinkPasswordModal: jest.fn().mockImplementation((_options, cb) => cb('password'))
showQuickLinkPasswordModal: jest.fn().mockImplementation((_options, cb) => cb('password')),
getDefaultLinkPermissions: jest.fn()
}))

const selectors = Object.freeze({
Expand Down Expand Up @@ -192,6 +195,7 @@ describe('EmbedActions', () => {
const { wrapper } = getWrapper({
selectedFiles: [{ id: 1 }],
abilities: [{ action: 'create-all', subject: 'PublicLink' }],
defaultLinkPermissions: SharePermissionBit.Read,
capabilities: jest.fn().mockReturnValue({
files_sharing: { public: { password: { enforced_for: { read_only: true } } } }
})
Expand Down Expand Up @@ -245,13 +249,15 @@ function getWrapper(
abilities = [],
capabilities = jest.fn().mockReturnValue({}),
configuration = { options: {} },
currentFolder = {}
currentFolder = {},
defaultLinkPermissions = SharePermissionBit.Internal
} = {
selectedFiles: [],
abilities: [],
capabilities: jest.fn().mockReturnValue({})
}
) {
jest.mocked(getDefaultLinkPermissions).mockReturnValue(defaultLinkPermissions)
const storeOptions = {
...defaultStoreMockOptions,
getters: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {
} from 'web-test-helpers'
import { mockDeep } from 'jest-mock-extended'
import { Resource } from '@ownclouders/web-client'
import { SharePermissions } from '@ownclouders/web-client/src/helpers/share'
import { SharePermissionBit, SharePermissions } from '@ownclouders/web-client/src/helpers/share'
import { AbilityRule } from '@ownclouders/web-client/src/helpers/resource/types'
import { getDefaultLinkPermissions } from '@ownclouders/web-pkg'

const defaultLinksList = [
{
Expand Down Expand Up @@ -38,6 +39,11 @@ const selectors = {
const linkListItemNameAndCopy = 'name-and-copy-stub'
const linkListItemDetailsAndEdit = 'details-and-edit-stub'

jest.mock('@ownclouders/web-pkg', () => ({
...jest.requireActual('@ownclouders/web-pkg'),
getDefaultLinkPermissions: jest.fn()
}))

describe('FileLinks', () => {
describe('links', () => {
describe('when links list is not empty', () => {
Expand Down Expand Up @@ -143,31 +149,39 @@ describe('FileLinks', () => {
expect(availableRoleOptions[0].permissions()).toEqual([SharePermissions.internal])
expect(isModifiable).toBeTruthy()
})
it('creates new links with permission 0', async () => {
const { wrapper, storeOptions } = getWrapper({ abilities: [] })
await wrapper.find(selectors.linkAddButton).trigger('click')
expect(storeOptions.modules.Files.actions.addLink).toHaveBeenCalledTimes(1)
expect(storeOptions.modules.Files.actions.addLink).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
params: expect.objectContaining({
permissions: '0'
})
describe('new links', () => {
it.each([SharePermissionBit.Internal, SharePermissionBit.Read])(
'creates new links according to the default link permissions',
async (defaultLinkPermissions) => {
const { wrapper, storeOptions } = getWrapper({ abilities: [], defaultLinkPermissions })
await wrapper.find(selectors.linkAddButton).trigger('click')
expect(storeOptions.modules.Files.actions.addLink).toHaveBeenCalledTimes(1)
expect(storeOptions.modules.Files.actions.addLink).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
params: expect.objectContaining({
permissions: defaultLinkPermissions.toString()
})
})
})
)
})
)
}
)
})
})

function getWrapper({
resource = mockDeep<Resource>({ isFolder: false, canShare: () => true }),
links = defaultLinksList,
abilities = [{ action: 'create-all', subject: 'PublicLink' }]
abilities = [{ action: 'create-all', subject: 'PublicLink' }],
defaultLinkPermissions = 0
}: {
resource?: Resource
links?: typeof defaultLinksList
abilities?: AbilityRule[]
defaultLinkPermissions?: number
} = {}) {
jest.mocked(getDefaultLinkPermissions).mockReturnValue(defaultLinkPermissions)
const storeOptions = {
...defaultStoreMockOptions,
getters: {
Expand Down
9 changes: 5 additions & 4 deletions packages/web-pkg/src/composables/capability/useCapability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
MediaTypeCapability,
PasswordPolicyCapability
} from '@ownclouders/web-client/src/ocs/capabilities'
import { SharePermissionBit } from '@ownclouders/web-client/src/helpers'

export const useCapability = <T>(
store: Store<any>,
Expand Down Expand Up @@ -39,10 +40,6 @@ export const useCapabilityGraphPersonalDataExport = createCapabilityComposable(
'graph.personal-data-export',
false
)
export const useCapabilityFilesSharingQuickLinkDefaultRole = createCapabilityComposable(
'files_sharing.quick_link.default_role',
'viewer'
)
export const useCapabilityFilesSharingResharing = createCapabilityComposable(
'files_sharing.resharing',
true
Expand Down Expand Up @@ -136,6 +133,10 @@ export const useCapabilityFilesSharingPublicAlias = createCapabilityComposable(
'files_sharing.public.alias',
false
)
export const useCapabilityFilesSharingPublicDefaultPermissions = createCapabilityComposable(
'files_sharing.public.default_permissions',
SharePermissionBit.Read
)
export const useCapabilityNotifications = createCapabilityComposable(
'notifications.ocs-endpoints',
[]
Expand Down
61 changes: 31 additions & 30 deletions packages/web-pkg/src/helpers/share/link.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { DateTime } from 'luxon'
import {
LinkShareRoles,
Share,
linkRoleInternalFolder,
linkRoleViewerFolder,
ShareTypes,
buildShare
buildShare,
SharePermissionBit
} from '@ownclouders/web-client/src/helpers/share'
import { Store } from 'vuex'
import { ClientService, PasswordPolicyService } from '../../services'
Expand Down Expand Up @@ -39,7 +37,7 @@ export interface CopyQuickLink extends CreateQuicklink {
// it has a fallback to the vue-use implementation.
//
// https://webkit.org/blog/10855/
const copyToClipboard = async (quickLinkUrl: string) => {
const copyToClipboard = (quickLinkUrl: string) => {
if (typeof ClipboardItem && navigator?.clipboard?.write) {
return navigator.clipboard.write([
new ClipboardItem({
Expand All @@ -52,7 +50,7 @@ const copyToClipboard = async (quickLinkUrl: string) => {
}
}
export const copyQuicklink = async (args: CopyQuickLink) => {
const { store, language, resource, clientService, passwordPolicyService } = args
const { ability, store, language, resource, clientService, passwordPolicyService } = args
const { $gettext } = language

const linkSharesForResource = await clientService.owncloudSdk.shares.getShares(resource.path, {
Expand Down Expand Up @@ -82,7 +80,9 @@ export const copyQuicklink = async (args: CopyQuickLink) => {
const isPasswordEnforced =
store.getters.capabilities?.files_sharing?.public?.password?.enforced_for?.read_only === true

if (unref(isPasswordEnforced)) {
const permissions = getDefaultLinkPermissions({ ability, store })

if (unref(isPasswordEnforced) && permissions > SharePermissionBit.Internal) {
return showQuickLinkPasswordModal(
{ $gettext, store, passwordPolicyService },
async (password: string) => {
Expand Down Expand Up @@ -125,33 +125,13 @@ export const copyQuicklink = async (args: CopyQuickLink) => {
}
}

export const createQuicklink = async (args: CreateQuicklink): Promise<Share> => {
export const createQuicklink = (args: CreateQuicklink): Promise<Share> => {
const { clientService, resource, store, password, language, ability } = args
const { $gettext } = language

const canCreatePublicLink = ability.can('create-all', 'PublicLink')
const allowResharing = store.state.user.capabilities.files_sharing?.resharing
const capabilitiesRoleName =
store.state.user.capabilities.files_sharing?.quick_link?.default_role ||
linkRoleViewerFolder.name
const canEdit = store.state.user.capabilities.files_sharing?.public?.can_edit || false
const canContribute = store.state.user.capabilities.files_sharing?.public?.can_contribute || false
const alias = store.state.user.capabilities.files_sharing?.public?.alias
const roleName = !canCreatePublicLink
? linkRoleInternalFolder.name
: capabilitiesRoleName || linkRoleViewerFolder.name
const permissions = LinkShareRoles.getByName(
roleName,
resource.isFolder,
canEdit,
canContribute,
alias
).bitmask(allowResharing)
const params: {
[key: string]: unknown
} = {
const params: Record<string, unknown> = {
name: $gettext('Link'),
permissions: permissions.toString(),
permissions: getDefaultLinkPermissions({ ability, store }).toString(),
quicklink: true
}

Expand Down Expand Up @@ -180,3 +160,24 @@ export const createQuicklink = async (args: CreateQuicklink): Promise<Share> =>
storageId: resource.fileId || resource.id
})
}

export const getDefaultLinkPermissions = ({
ability,
store
}: {
ability: Ability
store: Store<any>
}) => {
const canCreatePublicLink = ability.can('create-all', 'PublicLink')
if (!canCreatePublicLink) {
return SharePermissionBit.Internal
}

let defaultPermissions: number =
store.state.user.capabilities.files_sharing?.public?.default_permissions
if (defaultPermissions === undefined) {
defaultPermissions = SharePermissionBit.Read
}

return defaultPermissions
}
Loading

0 comments on commit 1867db1

Please sign in to comment.