Skip to content

Commit

Permalink
Add support for a public link permission
Browse files Browse the repository at this point in the history
  • Loading branch information
JammingBen committed Mar 6, 2023
1 parent 98ed5c4 commit e966f63
Show file tree
Hide file tree
Showing 13 changed files with 108 additions and 38 deletions.
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-public-link-permission
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Public link permission `PublicLink.Write.all`

Support for the public link permission `PublicLink.Write.all` has been added. Users without this permission won't be able to create public links (except for oC10 instances where this permission is being set implicitly).

https://github.com/owncloud/web/pull/8541
https://github.com/owncloud/web/issues/8540
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
appearance="raw"
class="oc-mr-xs quick-action-button oc-p-xs"
:class="`files-quick-action-${action.id}`"
@click="action.handler({ ...$language, item, client: $client, store: $store })"
@click="
action.handler({ ...$language, item, client: $client, store: $store, ability: $ability })
"
>
<oc-icon :name="action.icon" fill-type="line" />
</oc-button>
Expand Down
29 changes: 17 additions & 12 deletions packages/web-app-files/src/components/SideBar/Shares/FileLinks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
<oc-contextual-helper v-if="helpersEnabled" class="oc-pl-xs" v-bind="viaLinkHelp" />
</div>
<p
v-if="!canCreatePublicLinks"
v-if="!canCreateLinks"
data-testid="files-links-no-reshare-permissions-message"
class="oc-mt-m"
v-text="noResharePermsMessage"
/>
<div class="oc-mt-m">
<name-and-copy v-if="quicklink" :link="quicklink" />
<create-quick-link
v-else-if="canCreatePublicLinks"
v-else-if="canCreateLinks"
:expiration-date="expirationDate"
@create-public-link="checkLinkToCreate"
/>
Expand All @@ -31,7 +31,7 @@
/>
<hr class="oc-my-m" />
<oc-button
v-if="canCreatePublicLinks"
v-if="canCreateLinks"
id="files-file-link-add"
variation="primary"
appearance="raw"
Expand Down Expand Up @@ -117,7 +117,8 @@ import {
useCapabilityFilesSharingResharing,
useCapabilityFilesSharingPublicCanEdit,
useCapabilityFilesSharingPublicCanContribute,
useCapabilityFilesSharingPublicAlias
useCapabilityFilesSharingPublicAlias,
useAbility
} from 'web-pkg/src/composables'
import { shareViaLinkHelp, shareViaIndirectLinkHelp } from '../../../helpers/contextualHelpers'
import { LinkShareRoles, SharePermissions } from 'web-client/src/helpers/share'
Expand All @@ -140,6 +141,7 @@ export default defineComponent({
},
setup() {
const store = useStore()
const { can } = useAbility()
const linkListCollapsed = !store.getters.configuration.options.sidebar.shares.showAllOnLoad
const indirectLinkListCollapsed = ref(linkListCollapsed)
Expand All @@ -160,6 +162,7 @@ export default defineComponent({
return { ...share, key: 'indirect-link-' + share.id }
})
)
const canCreatePublicLinks = computed(() => can('create-all', 'PublicLink'))
return {
...useGraphClient(),
Expand All @@ -176,7 +179,8 @@ export default defineComponent({
linkListCollapsed,
outgoingLinks,
directLinks,
indirectLinks
indirectLinks,
canCreatePublicLinks
}
},
computed: {
Expand Down Expand Up @@ -255,7 +259,7 @@ export default defineComponent({
return shareViaIndirectLinkHelp
},
canCreatePublicLinks() {
canCreateLinks() {
if (this.resource.isReceivedShare() && !this.hasResharing) {
return false
}
Expand All @@ -269,7 +273,7 @@ export default defineComponent({
},
canEdit() {
return this.canCreatePublicLinks
return this.canCreateLinks
},
noResharePermsMessage() {
Expand Down Expand Up @@ -363,7 +367,7 @@ export default defineComponent({
this.checkLinkToCreate({
link: {
name: this.$gettext('Link'),
permissions: 1,
permissions: this.canCreatePublicLinks ? 1 : 0,
expiration: this.expirationDate.default,
password: false
}
Expand Down Expand Up @@ -452,7 +456,6 @@ export default defineComponent({
await this.addLink({
path,
client: this.$client,
$gettext: this.$gettext,
storageId: this.resource.fileId || this.resource.id,
params
}).catch((e) => {
Expand Down Expand Up @@ -541,14 +544,15 @@ export default defineComponent({
},
getAvailableRoleOptions(link) {
if (this.incomingParentShare && this.canCreatePublicLinks) {
if (this.incomingParentShare && this.canCreateLinks) {
return LinkShareRoles.filterByBitmask(
this.incomingParentShare.permissions,
this.resource.isFolder,
this.hasPublicLinkEditing,
this.hasPublicLinkContribute,
this.hasPublicLinkAliasSupport,
!!link.password
!!link.password,
this.canCreatePublicLinks
)
}
Expand All @@ -557,7 +561,8 @@ export default defineComponent({
this.hasPublicLinkEditing,
this.hasPublicLinkContribute,
this.hasPublicLinkAliasSupport,
!!link.password
!!link.password,
this.canCreatePublicLinks
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import { computed, defineComponent } from 'vue'
import { useAbility } from 'web-pkg'
export default defineComponent({
name: 'CreateQuickLink',
Expand All @@ -30,6 +31,11 @@ export default defineComponent({
}
},
emits: ['createPublicLink'],
setup() {
const { can } = useAbility()
const canCreatePublicLinks = computed(() => can('create-all', 'PublicLink'))
return { canCreatePublicLinks }
},
computed: {
heading() {
return this.$gettext('Quick link')
Expand All @@ -49,7 +55,7 @@ export default defineComponent({
this.$emit('createPublicLink', {
link: {
name: this.$gettext('Quicklink'),
permissions: 1,
permissions: this.canCreatePublicLinks ? 1 : 0,
expiration: this.expirationDate.enforced ? this.expirationDate.default : null,
quicklink: true,
password: false
Expand Down
6 changes: 4 additions & 2 deletions packages/web-app-files/src/helpers/share/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ import { Share } from 'web-client/src/helpers/share'
import { Store } from 'vuex'
import { clientService } from 'web-pkg/src/services'
import { useClipboard } from '@vueuse/core'
import { Ability } from 'web-pkg'

interface CreateQuicklink {
store: Store<any>
storageId?: any
resource: any
password?: string
$gettext: (string) => string
ability: Ability
}

export const createQuicklink = async (args: CreateQuicklink): Promise<Share> => {
const { resource, store, password, $gettext } = args
const { resource, store, password, $gettext, ability } = args
const params: { [key: string]: unknown } = {
name: $gettext('Quicklink'),
permissions: 1,
permissions: ability.can('create-all', 'PublicLink') ? '1' : '0',
quicklink: true
}

Expand Down
3 changes: 2 additions & 1 deletion packages/web-app-files/src/mixins/actions/createQuicklink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export default {
resource,
storageId: this.space?.id || resource?.fileId || resource?.id,
store,
$gettext: this.$gettext
$gettext: this.$gettext,
ability: this.$ability
})
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/web-app-files/src/quickActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ export default {

if (passwordEnforced) {
return showQuickLinkPasswordModal(ctx, async (password) => {
await createQuicklink({ ...ctx, resource: ctx.item, password })
await createQuicklink({ ...ctx, resource: ctx.item, password, ability: ctx.ability })
})
}

await createQuicklink({ ...ctx, resource: ctx.item })
await createQuicklink({ ...ctx, resource: ctx.item, ability: ctx.ability })
},
displayed: canShare
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from 'web-test-helpers'
import { mockDeep } from 'jest-mock-extended'
import { Resource } from 'web-client'
import { SharePermissions } from 'web-client/src/helpers/share'

const defaultLinksList = [
{
Expand Down Expand Up @@ -72,7 +73,7 @@ describe('FileLinks', () => {
expect(wrapper.find('oc-list-stub').exists()).toBeFalsy()
})
})
describe('when canCreatePublicLinks is set to true', () => {
describe('when canCreateLinks is set to true', () => {
it('should show a button to add a link', () => {
const { wrapper } = getWrapper()
expect(wrapper.find(selectors.linkAddButton).exists()).toBeTruthy()
Expand All @@ -88,7 +89,7 @@ describe('FileLinks', () => {
})
})
})
describe('when canCreatePublicLinks is set to false', () => {
describe('when canCreateLinks is set to false', () => {
it('should show the "no reshare permissions" message', () => {
const resource = mockDeep<Resource>({
path: '/lorem.txt',
Expand All @@ -101,11 +102,41 @@ describe('FileLinks', () => {
expect(wrapper.find(selectors.noResharePermissions).exists()).toBeTruthy()
})
})
describe('user does not have the permission to create public links', () => {
it('only shows internal role option', () => {
const resource = mockDeep<Resource>({
path: '/lorem.txt',
type: 'file',
isFolder: false,
isReceivedShare: jest.fn()
})
const { wrapper } = getWrapper({ resource, abilities: [] })
const availableRoleOptions = wrapper
.findComponent<any>(linkListItemDetailsAndEdit)
.props('availableRoleOptions')
expect(availableRoleOptions.length).toBe(1)
expect(availableRoleOptions[0].permissions()).toEqual([SharePermissions.internal])
})
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'
})
})
)
})
})
})

function getWrapper({
resource = mockDeep<Resource>({ isFolder: false, canShare: () => true }),
links = defaultLinksList
links = defaultLinksList,
abilities = [{ action: 'create-all', subject: 'PublicLink' }]
} = {}) {
const storeOptions = {
...defaultStoreMockOptions,
Expand All @@ -120,6 +151,7 @@ function getWrapper({
public: {
defaultPublicLinkShareName: 'public link name default',
expire_date: new Date(),
alias: true,
password: {
enforced_for: {
read_only: false,
Expand All @@ -136,9 +168,10 @@ function getWrapper({
defaultStoreMockOptions.modules.Files.getters.outgoingLinks.mockReturnValue(links)
const store = createStore(storeOptions)
return {
storeOptions,
wrapper: shallowMount(FileLinks, {
global: {
plugins: [...defaultPlugins(), store],
plugins: [...defaultPlugins({ abilities }), store],
renderStubDefaultSlot: true,
stubs: { OcButton: false },
provide: {
Expand Down
33 changes: 20 additions & 13 deletions packages/web-client/src/helpers/share/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,16 +361,17 @@ export abstract class LinkShareRoles {
canEditFile = false,
canContribute = false,
hasAliasLinks = false,
hasPassword = false
hasPassword = false,
canCreatePublicLinks = true
): ShareRole[] {
return [
...(hasAliasLinks && !hasPassword ? [linkRoleInternalFile, linkRoleInternalFolder] : []),
linkRoleViewerFile,
linkRoleViewerFolder,
...(canContribute ? [linkRoleContributorFolder] : []),
linkRoleEditorFolder,
linkRoleUploaderFolder,
...(canEditFile ? [linkRoleEditorFile] : [])
...(canCreatePublicLinks ? [linkRoleViewerFile] : []),
...(canCreatePublicLinks ? [linkRoleViewerFolder] : []),
...(canCreatePublicLinks && canContribute ? [linkRoleContributorFolder] : []),
...(canCreatePublicLinks ? [linkRoleEditorFolder] : []),
...(canCreatePublicLinks ? [linkRoleUploaderFolder] : []),
...(canCreatePublicLinks && canEditFile ? [linkRoleEditorFile] : [])
].filter((r) => r.folder === isFolder)
}

Expand All @@ -393,13 +394,19 @@ export abstract class LinkShareRoles {
canEditFile = false,
canContribute = false,
hasAliasLinks = false,
hasPassword = false
hasPassword = false,
canCreatePublicLinks = true
): ShareRole[] {
return this.list(isFolder, canEditFile, canContribute, hasAliasLinks, hasPassword).filter(
(r) => {
return bitmask === (bitmask | r.bitmask(false))
}
)
return this.list(
isFolder,
canEditFile,
canContribute,
hasAliasLinks,
hasPassword,
canCreatePublicLinks
).filter((r) => {
return bitmask === (bitmask | r.bitmask(false))
})
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/web-pkg/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type AbilitySubjects =
| 'Group'
| 'Language'
| 'Logo'
| 'PublicLink'
| 'Role'
| 'Setting'
| 'Space'
Expand Down
1 change: 1 addition & 0 deletions packages/web-runtime/src/services/auth/abilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const getAbilities = (
{ action: 'update-all', subject: 'Language' }
],
'change-logo.all': [{ action: 'update-all', subject: 'Logo' }],
'PublicLink.Write.all': [{ action: 'create-all', subject: 'PublicLink' }],
'role-management.all': [
{ action: 'create-all', subject: 'Role' },
{ action: 'delete-all', subject: 'Role' },
Expand Down
3 changes: 2 additions & 1 deletion packages/web-runtime/src/services/auth/userManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,12 @@ export class UserManager extends OidcUserManager {

private async fetchPermissions({ user, accessToken = '' }): Promise<any> {
const httpClient = clientService.httpAuthenticated(accessToken)
const oC10DefaultPermissions = ['PublicLink.Write.all']
try {
const {
data: { permissions }
} = await httpClient.post('/api/v0/settings/permissions-list', { account_uuid: user.uuid })
return permissions || []
return permissions || oC10DefaultPermissions
} catch (e) {
console.error(e)
return []
Expand Down
Loading

0 comments on commit e966f63

Please sign in to comment.