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

feat: respect default link permissions #10038

Merged
merged 1 commit into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Comment on lines +178 to +180
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defining the same default as fallback here as in useCapabilityFilesSharingPublicDefaultPermissions is a shortcoming of our architecture, see #9748.


return defaultPermissions
}
Loading